sently 0.3.3 → 0.4.1

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 +119 -0
  2. package/README.md +93 -0
  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,119 @@
1
+ # Changelog
2
+
3
+ ## [0.4.1] — 2026-05-30
4
+
5
+ ### Fixed
6
+
7
+ - CI: use locally installed tsc (node_modules/.bin/tsc) in build.ts
8
+ instead of bunx tsc to avoid runtime npm downloads in CI
9
+ - CI: add bun run build step to SMTP integration job so dist/ exists
10
+ before the integration test imports from 'sently'
11
+ - CI: improved smoke test error output with explicit .catch() handler
12
+ - CI: added FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 to all workflow jobs
13
+
14
+ ## [0.4.0] — 2026-05-30
15
+
16
+ ### Added
17
+
18
+ - `PreviewTransport` — writes emails to disk as .eml or HTML for local development
19
+ - `RetryTransport` — decorator transport with exponential/linear/fixed backoff
20
+ - `mailer.sendBulk()` — batch send with concurrency control and per-message callbacks
21
+ - `templatePlugin` + `simpleEngine` — zero-dependency {{variable}} template rendering
22
+ - `verify()` on all HTTP transports (Resend, SendGrid, Postmark, Mailgun, SES, Brevo)
23
+ returns typed `VerifyResult` instead of `boolean`
24
+ - `MailOptions.template` and `MailOptions.data` fields for template plugin integration
25
+ - `SESTransport` now accepts `dkim` config for signing raw MIME messages
26
+ - `attachment.path` basePath guard (opt-in) in resolveAttachments
27
+ - GitHub Actions CI matrix: unit tests (Bun), smoke test (Node 22), SMTP integration (Mailpit)
28
+
29
+ ### Fixed
30
+
31
+ - `detectRuntime()` priority hardened: Bun checked before Node.js process globals
32
+ - Cloudflare Workers detection uses positive signature (caches + UA), not absence of other runtimes
33
+
34
+ ## [0.3.4] — 2026-05-30
35
+
36
+ ### Fixed
37
+
38
+ - Stale `sendx` references in package.json, build.ts, PROGRESS.md
39
+ - JSR badge URL now matches jsr.json scope exactly
40
+ - OAuth2 refresh deduplication: `refreshPromise` cleared in `.finally()`
41
+ to correctly handle rejected refresh attempts
42
+ - SMTPPool.close() now sets draining flag, rejects new sends,
43
+ and uses Promise.allSettled to drain in-flight messages
44
+ - Audited all buildMIME() call sites — await confirmed present
45
+
46
+ ### Added
47
+
48
+ - `engines` field in package.json (Node >= 18, Bun >= 1.0)
49
+
50
+ ## [0.3.3] — 2026-05-29
51
+
52
+ ### Fixed
53
+
54
+ - Corrected JSR package name in README from `@sently/sently` to `@alialnaghmoush/sently`
55
+
56
+ ## [0.3.2] — 2026-05-29
57
+
58
+ ### Fixed
59
+
60
+ - Biome formatting in MIME header builder (`src/core/mime.ts`) so `bun lint` passes
61
+
62
+ ## [0.3.1] — 2026-05-29
63
+
64
+ ### Security
65
+
66
+ - Fixed CRLF header injection: `sanitizeHeaderValue()` strips CR/LF
67
+ from Subject, display names, and custom headers in MIME builder
68
+ - Fixed SMTP command injection: `MAIL FROM` and `RCPT TO` throw
69
+ `SMTPError` when address contains CR or LF
70
+ - Fixed email address validation: `isValidEmail()` rejects strings
71
+ containing CR, LF, or TAB
72
+ - Fixed OAuth2 refresh race condition: concurrent `getAccessToken()`
73
+ calls now share a single in-flight refresh Promise
74
+ - Added `console.warn` when `rejectUnauthorized: false` is set
75
+ in Node.js and Bun adapters
76
+ - Added security note in README for `attachment.path`
77
+
78
+ ## [0.3.0] — 2026-05-29
79
+
80
+ ### Added
81
+
82
+ - Plugin system: `plugins` array in `createMailer()` config
83
+ Plugins are `(options: MailOptions) => MailOptions | Promise<MailOptions>` functions
84
+ that run sequentially before message construction
85
+ - `MailgunTransport` — Mailgun HTTP API (multipart/form-data)
86
+ - `SESTransport` — AWS SES v2 HTTP API with SigV4 signing (Web Crypto)
87
+ - `BrevoTransport` — Brevo (formerly Sendinblue) HTTP API
88
+ - `TLSOptions.minVersion` — set minimum TLS version for legacy SMTP servers
89
+
90
+ ### Parity milestone
91
+
92
+ sently now covers ~98% of Nodemailer feature parity for modern use cases.
93
+ Remaining gaps (SOCKS proxy, iCal) are out of scope by design.
94
+
95
+ ## [0.2.0] — 2026-05-29
96
+
97
+ ### Added
98
+
99
+ - DKIM signing (RSA-SHA256 and Ed25519-SHA256) via `SMTPConfig.dkim`
100
+ - OAuth2 / XOAUTH2 authentication via `SMTPAuth.type = 'OAUTH2'`
101
+ - Connection pooling via `SMTPConfig.pool` and `SMTPPool`
102
+ - Rate limiting via `PoolConfig.rateDelta` / `PoolConfig.rateLimit`
103
+ - CRAM-MD5 authentication (pure-JS HMAC-MD5)
104
+
105
+ ### Changed
106
+
107
+ - npm package name is `sently`; JSR package name is `@sently/sently`
108
+ - `SMTPAuth.pass` is now optional (was required in v0.1)
109
+ - `buildMIME()` is now `async` when DKIM config is provided
110
+ - `selectAuthMethod` priority: XOAUTH2 > CRAM-MD5 > LOGIN > PLAIN
111
+ - `createMailer()` uses `SMTPPool` automatically when `pool: true`
112
+
113
+ ### Fixed
114
+
115
+ - CRAM-MD5 stub now fully implemented
116
+
117
+ ## [0.1.0] — 2026-05-29
118
+
119
+ Initial release.
package/README.md CHANGED
@@ -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.
@@ -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,