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.
- package/CHANGELOG.md +119 -0
- package/README.md +93 -0
- package/dist/adapters/bun.d.ts +35 -0
- package/dist/{src/adapters → adapters}/bun.js +1 -1
- package/dist/adapters/cf.d.ts +55 -0
- package/dist/{src/adapters → adapters}/cf.js +1 -1
- package/dist/adapters/deno.d.ts +48 -0
- package/dist/{src/adapters → adapters}/deno.js +1 -1
- package/dist/adapters/node.d.ts +35 -0
- package/dist/{src/adapters → adapters}/node.js +1 -1
- package/dist/auth/oauth2.d.ts +34 -0
- package/dist/{src/auth → auth}/oauth2.js +3 -3
- package/dist/{chunk-hdqpvsm8.js → chunk-bvxkmq94.js} +12 -3
- package/dist/{chunk-hdqpvsm8.js.map → chunk-bvxkmq94.js.map} +3 -3
- package/dist/{chunk-dhbe64fc.js → chunk-j6qw8ms6.js} +1 -1
- package/dist/{chunk-qb05tsqn.js → chunk-tjsgb3qb.js} +2 -221
- package/dist/chunk-tjsgb3qb.js.map +11 -0
- package/dist/chunk-z3eq2t1d.js +244 -0
- package/dist/chunk-z3eq2t1d.js.map +10 -0
- package/dist/core/address.d.ts +21 -0
- package/dist/core/base64.d.ts +27 -0
- package/dist/core/cram-md5.d.ts +17 -0
- package/dist/core/dkim.d.ts +22 -0
- package/dist/core/mime.d.ts +13 -0
- package/dist/core/plugin.d.ts +23 -0
- package/dist/core/sigv4.d.ts +57 -0
- package/dist/core/smtp.d.ts +90 -0
- package/dist/core/types.d.ts +291 -0
- package/dist/detect.d.ts +15 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +29 -0
- package/dist/{src/index.js.map → index.js.map} +1 -1
- package/dist/plugins/template.d.ts +61 -0
- package/dist/plugins/template.js +29 -0
- package/dist/plugins/template.js.map +10 -0
- package/dist/pool/connection.d.ts +25 -0
- package/dist/pool/pool.d.ts +59 -0
- package/dist/{src/pool → pool}/pool.js +26 -14
- package/dist/pool/pool.js.map +11 -0
- package/dist/transports/brevo.d.ts +20 -0
- package/dist/{src/transports → transports}/brevo.js +32 -4
- package/dist/transports/brevo.js.map +10 -0
- package/dist/transports/mailgun.d.ts +22 -0
- package/dist/{src/transports → transports}/mailgun.js +29 -4
- package/dist/transports/mailgun.js.map +10 -0
- package/dist/transports/postmark.d.ts +24 -0
- package/dist/{src/transports → transports}/postmark.js +33 -4
- package/dist/transports/postmark.js.map +10 -0
- package/dist/transports/preview.d.ts +15 -0
- package/dist/transports/preview.js +73 -0
- package/dist/transports/preview.js.map +10 -0
- package/dist/transports/resend.d.ts +26 -0
- package/dist/{src/transports → transports}/resend.js +28 -4
- package/dist/transports/resend.js.map +10 -0
- package/dist/transports/resolve-attachments.d.ts +12 -0
- package/dist/transports/retry.d.ts +21 -0
- package/dist/transports/retry.js +79 -0
- package/dist/transports/retry.js.map +10 -0
- package/dist/transports/sendgrid.d.ts +24 -0
- package/dist/{src/transports → transports}/sendgrid.js +33 -4
- package/dist/transports/sendgrid.js.map +10 -0
- package/dist/transports/ses.d.ts +25 -0
- package/dist/{src/transports → transports}/ses.js +45 -6
- package/dist/{src/transports → transports}/ses.js.map +3 -3
- package/dist/transports/smtp.d.ts +52 -0
- package/dist/transports/smtp.js +27 -0
- package/dist/{src/transports → transports}/smtp.js.map +1 -1
- package/package.json +25 -4
- package/dist/chunk-qb05tsqn.js.map +0 -12
- package/dist/src/index.js +0 -18
- package/dist/src/pool/pool.js.map +0 -11
- package/dist/src/transports/brevo.js.map +0 -10
- package/dist/src/transports/mailgun.js.map +0 -10
- package/dist/src/transports/postmark.js.map +0 -10
- package/dist/src/transports/resend.js.map +0 -10
- package/dist/src/transports/sendgrid.js.map +0 -10
- package/dist/src/transports/smtp.js +0 -25
- /package/dist/{src/adapters → adapters}/bun.js.map +0 -0
- /package/dist/{src/adapters → adapters}/cf.js.map +0 -0
- /package/dist/{src/adapters → adapters}/deno.js.map +0 -0
- /package/dist/{src/adapters → adapters}/node.js.map +0 -0
- /package/dist/{src/auth → auth}/oauth2.js.map +0 -0
- /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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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 "
|
|
6
|
-
import"
|
|
7
|
-
import"
|
|
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
|
|
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=
|
|
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[]
|
|
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": ";;;;;;;;AAOO,SAAS,cAAc,CAAC,OAAgC;AAAA,EAC7D,IAAI,MAAM,QAAQ,KAAK,GAAG;AAAA,IACxB,OAAO,MAAM,QAAQ,CAAC,SAAS,eAAe,IAAI,CAAC;AAAA,EACrD;AAAA,EAEA,IAAI,OAAO,UAAU,UAAU;AAAA,IAC7B,OAAO,CAAC,KAAK,MAAM,CAAC;AAAA,EACtB;AAAA,EAEA,MAAM,UAAU,MAAM,KAAK;AAAA,EAC3B,IAAI,CAAC,SAAS;AAAA,IACZ,OAAO,CAAC;AAAA,EACV;AAAA,EAEA,OAAO,iBAAiB,OAAO,EAAE,IAAI,kBAAkB;AAAA;AAalD,SAAS,YAAY,CAAC,SAA0B;AAAA,EACrD,IAAI,QAAQ,MAAM;AAAA,IAChB,MAAM,OAAO,aAAa,QAAQ,IAAI;AAAA,IACtC,OAAO,GAAG,SAAS,QAAQ;AAAA,EAC7B;AAAA,EACA,OAAO,QAAQ;AAAA;AAMV,SAAS,aAAa,CAAC,OAA+B;AAAA,EAC3D,OAAO,eAAe,KAAK,EAAE,IAAI,CAAC,SAAS,KAAK,OAAO;AAAA;AAWzD,SAAS,gBAAgB,CAAC,OAAyB;AAAA,EACjD,MAAM,QAAkB,CAAC;AAAA,EACzB,IAAI,UAAU;AAAA,EACd,IAAI,WAAW;AAAA,EACf,IAAI,UAAU;AAAA,EAEd,SAAS,IAAI,EAAG,IAAI,MAAM,QAAQ,KAAK;AAAA,IACrC,MAAM,OAAO,MAAM,MAAM;AAAA,IACzB,IAAI,SAAS,OAAO,MAAM,IAAI,OAAO,MAAM;AAAA,MACzC,WAAW,CAAC;AAAA,MACZ,WAAW;AAAA,MACX;AAAA,IACF;AAAA,IACA,IAAI,SAAS,OAAO,CAAC,UAAU;AAAA,MAC7B,UAAU;AAAA,MACV,WAAW;AAAA,MACX;AAAA,IACF;AAAA,IACA,IAAI,SAAS,OAAO,CAAC,UAAU;AAAA,MAC7B,UAAU;AAAA,MACV,WAAW;AAAA,MACX;AAAA,IACF;AAAA,IACA,IAAI,SAAS,OAAO,CAAC,YAAY,CAAC,SAAS;AAAA,MACzC,IAAI,QAAQ,KAAK,GAAG;AAAA,QAClB,MAAM,KAAK,QAAQ,KAAK,CAAC;AAAA,MAC3B;AAAA,MACA,UAAU;AAAA,MACV;AAAA,IACF;AAAA,IACA,WAAW;AAAA,EACb;AAAA,EAEA,IAAI,QAAQ,KAAK,GAAG;AAAA,IAClB,MAAM,KAAK,QAAQ,KAAK,CAAC;AAAA,EAC3B;AAAA,EAEA,OAAO;AAAA;AAGT,SAAS,kBAAkB,CAAC,OAAwB;AAAA,EAClD,MAAM,UAAU,MAAM,KAAK;AAAA,EAE3B,MAAM,aAAa,QAAQ,MAAM,sCAAsC;AAAA,EACvE,IAAI,YAAY;AAAA,IACd,MAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,IAAI,KAAK;AAAA,IACzD,MAAM,WAAW,WAAW,MAAM,IAAI,KAAK;AAAA,IAC3C,IAAI,MAAM;AAAA,MACR,OAAO,EAAE,MAAM,QAAQ;AAAA,IACzB;AAAA,IACA,OAAO,EAAE,QAAQ;AAAA,EACnB;AAAA,EAEA,IAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,GAAG;AAAA,IACpD,OAAO,EAAE,SAAS,QAAQ,MAAM,GAAG,EAAE,EAAE;AAAA,EACzC;AAAA,EAEA,OAAO,EAAE,SAAS,QAAQ;AAAA;;;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "7C59D40B68EA994E64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|