sently 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/README.md +86 -98
- package/dist/adapters/bun.js +1 -1
- package/dist/adapters/bun.js.map +1 -1
- package/dist/adapters/cf.d.ts +2 -2
- package/dist/adapters/cf.js +2 -2
- package/dist/adapters/cf.js.map +2 -2
- package/dist/adapters/deno.d.ts +2 -2
- package/dist/adapters/deno.js +1 -1
- package/dist/adapters/deno.js.map +1 -1
- package/dist/adapters/node.js +1 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/auth/oauth2.js +1 -1
- package/dist/{chunk-qnz9z0d4.js → chunk-4mvp3b85.js} +2 -2
- package/dist/chunk-4mvp3b85.js.map +10 -0
- package/dist/chunk-74rha3ac.js +4 -0
- package/dist/chunk-74rha3ac.js.map +10 -0
- package/dist/{chunk-n601jhpd.js → chunk-a1c87hn1.js} +2 -2
- package/dist/chunk-a1c87hn1.js.map +10 -0
- package/dist/chunk-cvrbfke9.js +13 -0
- package/dist/{chunk-w3fnjh04.js.map → chunk-cvrbfke9.js.map} +2 -2
- package/dist/{chunk-wzdcz88x.js → chunk-d5rwm3br.js} +2 -2
- package/dist/{chunk-wzdcz88x.js.map → chunk-d5rwm3br.js.map} +1 -1
- package/dist/{chunk-y4qxdyqn.js → chunk-gh5n759p.js} +3 -3
- package/dist/{chunk-y4qxdyqn.js.map → chunk-gh5n759p.js.map} +1 -1
- package/dist/{chunk-y72qggqf.js → chunk-gkkv7yqm.js} +2 -2
- package/dist/{chunk-y72qggqf.js.map → chunk-gkkv7yqm.js.map} +1 -1
- package/dist/chunk-ka6t8g77.js +4 -0
- package/dist/chunk-ka6t8g77.js.map +11 -0
- package/dist/{chunk-tfxje74k.js → chunk-kebhy657.js} +2 -2
- package/dist/{chunk-tfxje74k.js.map → chunk-kebhy657.js.map} +1 -1
- package/dist/{chunk-rszb1n1d.js → chunk-keksa6fx.js} +2 -2
- package/dist/{chunk-rszb1n1d.js.map → chunk-keksa6fx.js.map} +1 -1
- package/dist/{chunk-z7j2n9k8.js → chunk-r7ee9tk7.js} +2 -2
- package/dist/{chunk-z7j2n9k8.js.map → chunk-r7ee9tk7.js.map} +1 -1
- package/dist/{chunk-348wndn3.js → chunk-rm78s1ek.js} +2 -2
- package/dist/{chunk-348wndn3.js.map → chunk-rm78s1ek.js.map} +1 -1
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +2 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/smtp.js +2 -2
- package/dist/core/smtp.js.map +1 -1
- package/dist/core/types.d.ts +4 -8
- package/dist/detect.d.ts +1 -7
- package/dist/detect.js +2 -2
- package/dist/detect.js.map +3 -4
- package/dist/dkim.js +2 -2
- package/dist/dkim.js.map +1 -1
- package/dist/idempotency.js +2 -2
- package/dist/idempotency.js.map +2 -2
- package/dist/index.d.ts +12 -98
- package/dist/index.js +4 -98
- package/dist/mailer.js +2 -2
- package/dist/mailer.js.map +1 -1
- package/dist/plugins/react.js +2 -2
- package/dist/plugins/react.js.map +1 -1
- package/dist/plugins/template.js +1 -1
- package/dist/pool/pool.js +2 -2
- package/dist/pool/pool.js.map +1 -1
- package/dist/smtp-mailer.d.ts +23 -0
- package/dist/smtp-mailer.js +3 -0
- package/dist/smtp-mailer.js.map +10 -0
- package/dist/transports/brevo.js +2 -2
- package/dist/transports/brevo.js.map +2 -2
- package/dist/transports/mailgun.js +2 -2
- package/dist/transports/mailgun.js.map +3 -3
- package/dist/transports/postmark.js +2 -2
- package/dist/transports/postmark.js.map +3 -3
- package/dist/transports/preview.js +2 -2
- package/dist/transports/preview.js.map +1 -1
- package/dist/transports/resend.js +2 -2
- package/dist/transports/resend.js.map +2 -2
- package/dist/transports/retry.js +2 -2
- package/dist/transports/retry.js.map +4 -3
- package/dist/transports/sendgrid.js +2 -2
- package/dist/transports/sendgrid.js.map +2 -2
- package/dist/transports/ses.js +2 -2
- package/dist/transports/ses.js.map +2 -2
- package/dist/transports/smtp.d.ts +1 -0
- package/dist/transports/smtp.js +2 -2
- package/dist/transports/smtp.js.map +1 -1
- package/dist/webhooks/brevo.js +1 -1
- package/dist/webhooks/mailgun.js +1 -1
- package/dist/webhooks/postmark.js +1 -1
- package/dist/webhooks/resend.js +1 -1
- package/dist/webhooks/sendgrid.js +1 -1
- package/dist/webhooks/ses.js +1 -1
- package/dist/webhooks/timing-safe-equal.js +1 -1
- package/package.json +5 -1
- package/dist/chunk-3y1tr1vt.js +0 -4
- package/dist/chunk-3y1tr1vt.js.map +0 -11
- package/dist/chunk-n601jhpd.js.map +0 -10
- package/dist/chunk-qnz9z0d4.js.map +0 -10
- package/dist/chunk-t0en64h9.js +0 -4
- package/dist/chunk-t0en64h9.js.map +0 -10
- package/dist/chunk-w3fnjh04.js +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.7.0] — 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Breaking + Migration
|
|
8
|
+
|
|
9
|
+
Summary:
|
|
10
|
+
|
|
11
|
+
- **Transport-only `createMailer`** — passing SMTP config (`host` / `port` / `auth`) to
|
|
12
|
+
`createMailer` from `sently` or `sently/mailer` throws `SentlyError` (`INVALID_CONFIG`)
|
|
13
|
+
at factory time with guidance to use `createSMTPMailer`.
|
|
14
|
+
- **`sently/smtp`** — removed `createMailer` alias; export is **`createSMTPMailer` only**
|
|
15
|
+
(also available from main `"sently"`).
|
|
16
|
+
- **`CreateMailerOptions`** removed — use **`TransportMailerOptions`** or
|
|
17
|
+
**`SMTPMailerOptions`** instead.
|
|
18
|
+
- **Main barrel slimmed** — `"sently"` no longer re-exports transports, webhook parsers,
|
|
19
|
+
idempotency, DKIM helpers, template plugin, or `SMTPPool`. Import from subpaths
|
|
20
|
+
(e.g. `sently/transports/resend`, `sently/webhooks`).
|
|
21
|
+
- **Prior breaks documented in one place:** v0.5.1 react barrel move; v0.6.2
|
|
22
|
+
transport-only main `createMailer`; v0.5.x bulk-send behavior (native batch, default
|
|
23
|
+
2 req/s throttle, `SendResult.batchError`).
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`INVALID_CONFIG`** stable error code for misconfigured mailer factories
|
|
28
|
+
- **`SMTPMailerOptions`** type for SMTP factory config
|
|
29
|
+
- README decision-tree section and Microsoft 365 OAuth2 example
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **`BulkSendOptions.concurrency` TSDoc** — documents default `1` (matches implementation)
|
|
34
|
+
- Bundle size budgets updated for slimmer main entry
|
|
35
|
+
- JSDoc and README examples use explicit subpath imports
|
|
36
|
+
|
|
37
|
+
## [0.6.2] — 2026-05-31
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`sently/smtp`** subpath — SMTP `createMailer` (`host` / `port` / `auth`, pooling,
|
|
42
|
+
adapters) isolated from the main entry for smaller HTTP app bundles
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **Breaking:** main `createMailer` from `sently` is transport-only (same as
|
|
47
|
+
`sently/mailer`). SMTP relay apps use `import { createMailer } from "sently/smtp"`
|
|
48
|
+
or `import { createSMTPMailer } from "sently"`.
|
|
49
|
+
- **`RetryTransport`** — auth-failure detection uses `SentlyError.sentlyCode` instead
|
|
50
|
+
of importing `SMTPError` (leaner `sently/mailer` graph).
|
|
51
|
+
- **`mailer.ts`** — retry hook wiring duck-types `setMailerOnRetry` (no static
|
|
52
|
+
`RetryTransport` import).
|
|
53
|
+
- **Build** — remove `dist/` before each `bun run build` to avoid stale chunks in
|
|
54
|
+
published tarballs (~137 KB npm tarball vs ~424 KB with stale chunks).
|
|
55
|
+
- Bundle size budgets and README tables updated for measured gzip sizes (main entry
|
|
56
|
+
~2.4 KB; HTTP stack ~6 KB).
|
|
57
|
+
|
|
5
58
|
## [0.6.1] — 2026-05-31
|
|
6
59
|
|
|
7
60
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# sently
|
|
2
2
|
|
|
3
3
|
> Nodemailer hasn't been updated in years, doesn't run on Bun or Deno, and ships at 220KB.
|
|
4
|
-
> sently is the modern replacement — same familiar API, runs everywhere, HTTP stacks from ~
|
|
4
|
+
> sently is the modern replacement — same familiar API, runs everywhere, HTTP stacks from ~6 KB via `sently/mailer`.
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
bun add sently
|
|
@@ -14,13 +14,15 @@ bun add sently
|
|
|
14
14
|
[](#)
|
|
15
15
|
[](https://github.com/alialnaghmoush/sently)
|
|
16
16
|
|
|
17
|
+
> **Pre-1.0 — API may change.** sently is pre-1.0 and the public API is still being refined ahead of a stable v1.0.0. Breaking changes can land in any 0.x release; review the [CHANGELOG](CHANGELOG.md) before upgrading. Pin an exact version (e.g. `"sently": "0.7.0"`) for production until v1.0.0.
|
|
18
|
+
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Why not Nodemailer?
|
|
20
22
|
|
|
21
23
|
| Feature | Nodemailer | sently |
|
|
22
24
|
|---------|-----------|--------|
|
|
23
|
-
| Bundle size | ~220 KB always | ~6
|
|
25
|
+
| Bundle size | ~220 KB always | ~6 KB HTTP · ~15 KB SMTP |
|
|
24
26
|
| Runtimes | Node.js only | Node, Bun, Deno, CF Workers |
|
|
25
27
|
| Module format | CommonJS | ESM only |
|
|
26
28
|
| Dependencies | 3 | 0 |
|
|
@@ -97,8 +99,9 @@ bunx jsr add @alialnaghmoush/sently
|
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
```typescript
|
|
100
|
-
import { createMailer } from "sently/mailer"; // HTTP transports
|
|
101
|
-
import {
|
|
102
|
+
import { createMailer } from "sently/mailer"; // HTTP transports (~3 KB)
|
|
103
|
+
import { createSMTPMailer } from "sently/smtp"; // SMTP host/port (~15 KB)
|
|
104
|
+
// Or: import { createSMTPMailer } from "sently";
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
---
|
|
@@ -108,9 +111,9 @@ import { createMailer as createSMTPMailer } from "sently"; // SMTP host/port con
|
|
|
108
111
|
### SMTP with auto-detected adapter
|
|
109
112
|
|
|
110
113
|
```typescript
|
|
111
|
-
import {
|
|
114
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
112
115
|
|
|
113
|
-
const mailer = await
|
|
116
|
+
const mailer = await createSMTPMailer({
|
|
114
117
|
host: "smtp.example.com",
|
|
115
118
|
port: 587,
|
|
116
119
|
auth: { user: "you@example.com", pass: "secret" },
|
|
@@ -148,12 +151,12 @@ await mailer.send({
|
|
|
148
151
|
### Cloudflare Worker
|
|
149
152
|
|
|
150
153
|
```typescript
|
|
151
|
-
import {
|
|
154
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
152
155
|
import { CloudflareAdapter } from "sently/adapters/cf";
|
|
153
156
|
|
|
154
157
|
export default {
|
|
155
158
|
async fetch() {
|
|
156
|
-
const mailer = await
|
|
159
|
+
const mailer = await createSMTPMailer({
|
|
157
160
|
host: "smtp.example.com",
|
|
158
161
|
port: 587,
|
|
159
162
|
auth: { user: "relay@example.com", pass: "secret" },
|
|
@@ -178,17 +181,18 @@ export default {
|
|
|
178
181
|
|
|
179
182
|
| Runtime | Import | Notes |
|
|
180
183
|
|---------|--------|-------|
|
|
181
|
-
| Node.js (auto) | `
|
|
184
|
+
| Node.js (auto) | `createSMTPMailer` from `sently/smtp` | Auto-detected adapter |
|
|
182
185
|
| Node.js (explicit) | `sently/adapters/node` → `NodeAdapter` | Reference implementation |
|
|
183
|
-
| Bun (auto) | `
|
|
186
|
+
| Bun (auto) | `createSMTPMailer` from `sently/smtp` | Auto-detected adapter |
|
|
184
187
|
| Bun (explicit) | `sently/adapters/bun` → `BunAdapter` | Node compat layer |
|
|
185
188
|
| Deno | `sently/adapters/deno` → `DenoAdapter` | Native `Deno.startTls` |
|
|
186
189
|
| Cloudflare Workers | `sently/adapters/cf` → `CloudflareAdapter` | `cloudflare:sockets` |
|
|
187
190
|
|
|
188
191
|
```typescript
|
|
192
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
189
193
|
import { NodeAdapter } from "sently/adapters/node";
|
|
190
194
|
|
|
191
|
-
const mailer = await
|
|
195
|
+
const mailer = await createSMTPMailer({
|
|
192
196
|
host: "smtp.example.com",
|
|
193
197
|
adapter: new NodeAdapter({ secure: false }),
|
|
194
198
|
auth: { user: "you@example.com", pass: "secret" },
|
|
@@ -217,7 +221,7 @@ const mailer = await createMailer({ transport });
|
|
|
217
221
|
await mailer.verify(); // test connection + auth
|
|
218
222
|
```
|
|
219
223
|
|
|
220
|
-
|
|
224
|
+
For relay config (`host` / `port` / `auth`), prefer [`sently/smtp`](#smtp-with-auto-detected-adapter). Use `mailer` + `SMTPTransport` when you need an explicit adapter or transport-level options.
|
|
221
225
|
|
|
222
226
|
**AUTH methods:** XOAUTH2, CRAM-MD5, LOGIN, and PLAIN (auto-negotiated from EHLO unless `auth.type` is set).
|
|
223
227
|
|
|
@@ -226,7 +230,9 @@ Use `sently/mailer` instead of `sently` when passing `{ transport }` — keeps H
|
|
|
226
230
|
#### DKIM signing
|
|
227
231
|
|
|
228
232
|
```typescript
|
|
229
|
-
|
|
233
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
234
|
+
|
|
235
|
+
const mailer = await createSMTPMailer({
|
|
230
236
|
host: "smtp.example.com",
|
|
231
237
|
auth: { user: "you@example.com", pass: "secret" },
|
|
232
238
|
dkim: {
|
|
@@ -242,9 +248,9 @@ Pass `dkim` on SMTP config or use `signDKIM` from `sently/dkim` directly. MIME l
|
|
|
242
248
|
#### Gmail OAuth2 (XOAUTH2)
|
|
243
249
|
|
|
244
250
|
```typescript
|
|
245
|
-
import {
|
|
251
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
246
252
|
|
|
247
|
-
const mailer = await
|
|
253
|
+
const mailer = await createSMTPMailer({
|
|
248
254
|
host: "smtp.gmail.com",
|
|
249
255
|
port: 465,
|
|
250
256
|
secure: true,
|
|
@@ -261,12 +267,35 @@ const mailer = await createMailer({
|
|
|
261
267
|
});
|
|
262
268
|
```
|
|
263
269
|
|
|
264
|
-
Microsoft 365
|
|
270
|
+
#### Microsoft 365 OAuth2 (XOAUTH2)
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { MICROSOFT_TOKEN_URL } from "sently";
|
|
274
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
275
|
+
|
|
276
|
+
const mailer = await createSMTPMailer({
|
|
277
|
+
host: "smtp.office365.com",
|
|
278
|
+
port: 587,
|
|
279
|
+
auth: {
|
|
280
|
+
type: "OAUTH2",
|
|
281
|
+
user: "you@yourtenant.onmicrosoft.com",
|
|
282
|
+
oauth2: {
|
|
283
|
+
user: "you@yourtenant.onmicrosoft.com",
|
|
284
|
+
clientId: process.env.AZURE_CLIENT_ID!,
|
|
285
|
+
clientSecret: process.env.AZURE_CLIENT_SECRET!,
|
|
286
|
+
refreshToken: process.env.AZURE_REFRESH_TOKEN!,
|
|
287
|
+
tokenUrl: MICROSOFT_TOKEN_URL,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
265
292
|
|
|
266
293
|
#### Connection pooling
|
|
267
294
|
|
|
268
295
|
```typescript
|
|
269
|
-
|
|
296
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
297
|
+
|
|
298
|
+
const mailer = await createSMTPMailer({
|
|
270
299
|
host: "smtp.example.com",
|
|
271
300
|
pool: true,
|
|
272
301
|
maxConnections: 5,
|
|
@@ -387,7 +416,7 @@ const mailer = await createMailer({
|
|
|
387
416
|
|
|
388
417
|
Hooks are fully optional and zero-cost when unset. A throwing hook does **not** break the send — in non-production environments the error is logged with `console.warn` and the send continues. Pair `onRetry` with `RetryTransport` for per-attempt retry metrics.
|
|
389
418
|
|
|
390
|
-
Works with both `sently/mailer` (`{ transport, hooks }`) and SMTP config (`{ host, auth, hooks }`).
|
|
419
|
+
Works with both `sently/mailer` (`{ transport, hooks }`) and SMTP config via `createSMTPMailer` (`{ host, auth, hooks }`).
|
|
391
420
|
|
|
392
421
|
### IdempotencyTransport
|
|
393
422
|
|
|
@@ -421,14 +450,15 @@ Resend sends the `Idempotency-Key` HTTP header natively. Supply a shared store (
|
|
|
421
450
|
Plugins transform `MailOptions` before the transport builds and sends the message. They run sequentially — each receives the output of the previous plugin.
|
|
422
451
|
|
|
423
452
|
```typescript
|
|
424
|
-
import {
|
|
453
|
+
import type { MailOptions } from "sently";
|
|
454
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
425
455
|
|
|
426
456
|
const addFooter = (options: MailOptions) => ({
|
|
427
457
|
...options,
|
|
428
458
|
html: (options.html ?? "") + '<p style="color:#999">Unsubscribe</p>',
|
|
429
459
|
});
|
|
430
460
|
|
|
431
|
-
const mailer = await
|
|
461
|
+
const mailer = await createSMTPMailer({
|
|
432
462
|
host: "smtp.resend.com",
|
|
433
463
|
port: 465,
|
|
434
464
|
secure: true,
|
|
@@ -553,9 +583,6 @@ Parsers: Resend, SendGrid, Postmark, Mailgun, SES, Brevo. Optional HMAC verifica
|
|
|
553
583
|
|
|
554
584
|
## Attachments
|
|
555
585
|
|
|
556
|
-
> ⚠️ **Security note**: `attachment.path` reads files from disk.
|
|
557
|
-
> Never pass user-controlled paths without validation.
|
|
558
|
-
|
|
559
586
|
### In-memory (all runtimes)
|
|
560
587
|
|
|
561
588
|
```typescript
|
|
@@ -576,6 +603,8 @@ await mailer.send({
|
|
|
576
603
|
|
|
577
604
|
### File path (Node.js / Bun / Deno only)
|
|
578
605
|
|
|
606
|
+
`attachment.path` reads from disk — see [Security](#security) (Attachments) for validation and `basePath`.
|
|
607
|
+
|
|
579
608
|
```typescript
|
|
580
609
|
attachments: [
|
|
581
610
|
{
|
|
@@ -594,7 +623,8 @@ On Cloudflare Workers and browsers, use `content: Uint8Array` — `attachment.pa
|
|
|
594
623
|
All transport errors extend `SentlyError` for unified handling while preserving existing class names and properties:
|
|
595
624
|
|
|
596
625
|
```typescript
|
|
597
|
-
import { SentlyError
|
|
626
|
+
import { SentlyError } from "sently/errors";
|
|
627
|
+
import { SMTPError } from "sently/transports/smtp";
|
|
598
628
|
import { ResendError } from "sently/transports/resend";
|
|
599
629
|
// Each HTTP transport exports its own error class:
|
|
600
630
|
// SendGridError → sently/transports/sendgrid
|
|
@@ -667,105 +697,61 @@ MIME attachment filenames and custom attachment headers are likewise sanitized a
|
|
|
667
697
|
|
|
668
698
|
---
|
|
669
699
|
|
|
670
|
-
## Bundle
|
|
671
|
-
|
|
672
|
-
All sizes are **minified + gzip**, measured by bundling each import path in isolation (`bun run measure:size`). CI enforces budgets on key entries (`bun run check:size`). Node built-ins and `cloudflare:sockets` are external — same as in your app bundle.
|
|
700
|
+
## Bundle size
|
|
673
701
|
|
|
674
|
-
**
|
|
702
|
+
Sizes are **minified + gzip** per import path (`bun run measure:size`; CI: `bun run check:size`). Node built-ins and `cloudflare:sockets` are external.
|
|
675
703
|
|
|
676
|
-
|
|
704
|
+
Nodemailer ships **~220 KB** regardless of transport. sently tree-shakes by subpath — pick the entry that matches how you send:
|
|
677
705
|
|
|
678
|
-
|
|
|
679
|
-
|
|
680
|
-
| Resend, SendGrid,
|
|
681
|
-
| SMTP relay (`host` / `port`) | `sently` | **~15 KB**
|
|
682
|
-
|
|
|
683
|
-
| Unified errors | `sently/errors` or main barrel | **~0.3 KB** add-on |
|
|
684
|
-
| DKIM signing | `sently/dkim` or `dkim` option on send | **~1.7 KB** add-on, lazy-loaded by MIME |
|
|
685
|
-
| React Email | `sently/react` + peers | **~0.3 KB** plugin shell (+ `@react-email/render` peer) |
|
|
686
|
-
| Webhook parsing | `sently/webhooks` | **~0.5 KB** |
|
|
687
|
-
| Idempotency | `sently/idempotency` | **~1.2 KB** |
|
|
706
|
+
| How you send | Import | ~gzip |
|
|
707
|
+
|--------------|--------|-------|
|
|
708
|
+
| HTTP API (Resend, SendGrid, …) | `sently/mailer` + `sently/transports/<provider>` | **~6 KB** |
|
|
709
|
+
| SMTP relay (`host` / `port`) | `sently/smtp` (or `createSMTPMailer` from `sently`) | **~15 KB** |
|
|
710
|
+
| Transport only (no mailer wrapper) | `sently/transports/<provider>` | **~4–5 KB** |
|
|
688
711
|
|
|
689
712
|
```ts
|
|
690
|
-
//
|
|
713
|
+
// HTTP
|
|
691
714
|
import { createMailer } from "sently/mailer";
|
|
692
715
|
import { ResendTransport } from "sently/transports/resend";
|
|
693
716
|
|
|
694
|
-
//
|
|
695
|
-
import {
|
|
717
|
+
// SMTP
|
|
718
|
+
import { createSMTPMailer } from "sently/smtp";
|
|
696
719
|
```
|
|
697
720
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
| Use case | What you import | ~gzip |
|
|
701
|
-
|----------|-----------------|-------|
|
|
702
|
-
| HTTP — Resend | `sently/mailer` + `transports/resend` | **6.6 KB** |
|
|
703
|
-
| HTTP — SendGrid | `sently/mailer` + `transports/sendgrid` | **6.4 KB** |
|
|
704
|
-
| HTTP — transport only | `transports/resend` (call `.send()` directly) | **4.7 KB** |
|
|
705
|
-
| SMTP relay | `sently` + `{ host, port, auth }` | **15.3 KB** |
|
|
706
|
-
| SMTP + explicit adapter | `sently` + `adapters/node` | **15.3 KB** |
|
|
707
|
-
| Main entry + HTTP ⚠️ | `sently` + `transports/resend` | **16.2 KB** |
|
|
708
|
-
|
|
709
|
-
Adapters are **auto-selected at runtime** for SMTP unless you pass `adapter` explicitly. Only the adapter for your runtime is fetched (dynamic import).
|
|
721
|
+
Main `"sently"` exports shared types, `createMailer` (transport), `createSMTPMailer`, `detectRuntime`, OAuth2, and `SentlyError`. Transports, webhooks, idempotency, DKIM, and plugins are separate subpaths only.
|
|
710
722
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
| Export | ~gzip | Notes |
|
|
714
|
-
|--------|-------|-------|
|
|
715
|
-
| `sently/mailer` | 3.1 KB | `createMailer({ transport })` — plugins, hooks, `sendBulk`, rate-limited batch |
|
|
716
|
-
| `sently` | 15.3 KB | Full `createMailer` — SMTP config + lazy SMTP chunks |
|
|
717
|
-
| `sently/errors` | 0.3 KB | `SentlyError`, `httpStatusToSentlyCode`, `smtpCodeToSentlyCode` |
|
|
718
|
-
| `sently/dkim` | 1.7 KB | `signDKIM`, `importPrivateKey` — loaded when `dkim` option is set |
|
|
719
|
-
| `sently/react` | 0.3 KB | `reactPlugin()` — excludes `@react-email/render` peer |
|
|
720
|
-
| `sently/idempotency` | 1.2 KB | `IdempotencyTransport`, `MemoryIdempotencyStore` |
|
|
721
|
-
| `sently/webhooks` | 0.5 KB | Provider webhook parsers + optional signature verification |
|
|
722
|
-
|
|
723
|
-
### Transports
|
|
724
|
-
|
|
725
|
-
| Export | ~gzip | Protocol |
|
|
726
|
-
|--------|-------|----------|
|
|
727
|
-
| `sently/transports/resend` | 4.7 KB | HTTP (+ batch endpoint, rate limit) |
|
|
728
|
-
| `sently/transports/sendgrid` | 4.2 KB | HTTP (+ batch personalizations) |
|
|
729
|
-
| `sently/transports/postmark` | 4.1 KB | HTTP |
|
|
730
|
-
| `sently/transports/mailgun` | 4.1 KB | HTTP |
|
|
731
|
-
| `sently/transports/brevo` | 3.9 KB | HTTP |
|
|
732
|
-
| `sently/transports/ses` | 7.5 KB | HTTP (SigV4) |
|
|
733
|
-
| `sently/transports/smtp` | 10.3 KB | SMTP + MIME |
|
|
734
|
-
| `sently/transports/preview` | 6.4 KB | Dev disk preview |
|
|
735
|
-
|
|
736
|
-
HTTP transports share MIME/address parsing (~3.8 KB). SES is larger due to SigV4 signing.
|
|
737
|
-
|
|
738
|
-
### Adapters (SMTP socket layer)
|
|
739
|
-
|
|
740
|
-
| Export | ~gzip | Runtime |
|
|
741
|
-
|--------|-------|---------|
|
|
742
|
-
| `sently/adapters/node` | 1.2 KB | Node.js |
|
|
743
|
-
| `sently/adapters/bun` | 1.2 KB | Bun |
|
|
744
|
-
| `sently/adapters/deno` | 0.5 KB | Deno |
|
|
745
|
-
| `sently/adapters/cf` | 0.6 KB | Cloudflare Workers |
|
|
723
|
+
---
|
|
746
724
|
|
|
747
|
-
|
|
725
|
+
## Choosing an entrypoint
|
|
748
726
|
|
|
749
727
|
```
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
728
|
+
How do you send mail?
|
|
729
|
+
│
|
|
730
|
+
├─ HTTP API (Resend, SendGrid, …)
|
|
731
|
+
│ import { createMailer } from "sently/mailer"
|
|
732
|
+
│ import { ResendTransport } from "sently/transports/resend"
|
|
733
|
+
│ createMailer({ transport: new ResendTransport({ apiKey }) })
|
|
734
|
+
│
|
|
735
|
+
├─ SMTP relay (host / port / auth)
|
|
736
|
+
│ import { createSMTPMailer } from "sently/smtp"
|
|
737
|
+
│ createSMTPMailer({ host, port, auth })
|
|
738
|
+
│
|
|
739
|
+
└─ Custom / decorated transport (Retry, Idempotency, Preview)
|
|
740
|
+
import { createMailer } from "sently/mailer"
|
|
741
|
+
createMailer({ transport: new RetryTransport(inner) })
|
|
754
742
|
```
|
|
755
743
|
|
|
756
|
-
Regenerate tables after changes: `bun run measure:size` (full report) or `bun tools/measure-bundle-size.ts --markdown`.
|
|
757
|
-
|
|
758
744
|
---
|
|
759
745
|
|
|
760
746
|
## Migrating from Nodemailer
|
|
761
747
|
|
|
762
748
|
| Nodemailer | sently |
|
|
763
749
|
|------------|--------|
|
|
764
|
-
| `nodemailer.createTransport({...})` | `await
|
|
750
|
+
| `nodemailer.createTransport({...})` | `await createSMTPMailer({...})` or `createMailer({ transport })` |
|
|
765
751
|
| `transporter.sendMail(options)` | `mailer.send(options)` |
|
|
766
752
|
| `transporter.verify()` | `mailer.verify()` |
|
|
767
753
|
| `options.attachments[].path` | Same (Node/Bun/Deno); use `content` on edge |
|
|
768
|
-
| `import nodemailer from 'nodemailer'` | `import { createMailer } from 'sently/mailer'` (HTTP) or `'sently'`
|
|
754
|
+
| `import nodemailer from 'nodemailer'` | `import { createMailer } from 'sently/mailer'` (HTTP) or `createSMTPMailer` from `'sently/smtp'` |
|
|
769
755
|
| CommonJS | ESM only |
|
|
770
756
|
| Node.js only | Node, Bun, Deno, CF Workers |
|
|
771
757
|
|
|
@@ -780,6 +766,8 @@ import type {
|
|
|
780
766
|
SendResult,
|
|
781
767
|
Attachment,
|
|
782
768
|
SMTPConfig,
|
|
769
|
+
SMTPMailerOptions,
|
|
770
|
+
TransportMailerOptions,
|
|
783
771
|
} from "sently";
|
|
784
772
|
```
|
|
785
773
|
|
package/dist/adapters/bun.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import"../chunk-
|
|
1
|
+
import"../chunk-keksa6fx.js";import W from"node:net";import Q from"node:tls";function V(f){if(f.rejectUnauthorized===!1)console.warn("[sently] TLS certificate verification is disabled. Never use rejectUnauthorized: false in production.")}class X{socket=null;_secure;_connected=!1;connectionTimeout;tlsOptions;constructor(f={}){if(typeof Bun>"u")throw Error("BunAdapter requires the Bun runtime");this._secure=f.secure??!1,this.connectionTimeout=f.connectionTimeout??30000,this.tlsOptions=f.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(f,B){if(this._secure)await this.connectTls(f,B);else await this.connectPlain(f,B);this._connected=!0}async startTLS(f){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");let B=this.socket,y={...this.tlsOptions,...f};V(y),await new Promise((F,A)=>{let G=Q.connect({socket:B,servername:y.servername,rejectUnauthorized:y.rejectUnauthorized??!0,minVersion:y.minVersion});G.once("secureConnect",()=>{this.socket=G,this._secure=!0,F()}),G.once("error",A)})}async write(f){if(!this.socket)throw Error("Socket not connected");await new Promise((B,y)=>{this.socket?.write(Buffer.from(f),(F)=>{if(F)y(F);else B()})})}async*read(){if(!this.socket)throw Error("Socket not connected");let f=this.socket,B=[],y=null,F=!1,A=null,G=(H)=>{let I=new Uint8Array(H);if(y)y({value:I,done:!1}),y=null;else B.push(I)},J=(H)=>{if(A=H,F=!0,y)y({value:void 0,done:!0}),y=null},K=()=>{if(F=!0,y)y({value:void 0,done:!0}),y=null};f.on("data",G),f.on("error",J),f.on("close",K);try{while(!F||B.length>0){if(A)throw A;if(B.length>0){yield B.shift();continue}if(F)break;let H=await new Promise((I)=>{y=I});if(H.done)break;yield H.value}}finally{f.off("data",G),f.off("error",J),f.off("close",K)}}async close(){if(!this.socket)return;await new Promise((f)=>{this.socket?.end(()=>f())}),this.socket=null,this._connected=!1}connectPlain(f,B){return new Promise((y,F)=>{let A=W.connect({host:f,port:B},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}connectTls(f,B){return V(this.tlsOptions),new Promise((y,F)=>{let A=Q.connect({host:f,port:B,servername:this.tlsOptions.servername??f,rejectUnauthorized:this.tlsOptions.rejectUnauthorized??!0,minVersion:this.tlsOptions.minVersion},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}}export{X as BunAdapter};
|
|
2
2
|
|
|
3
3
|
//# debugId=94AAFD676203266764756E2164756E21
|
package/dist/adapters/bun.js.map
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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 {
|
|
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 { createSMTPMailer } from \"sently/smtp\";\n *\n * const mailer = await createSMTPMailer({\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 /** Underlying TCP or TLS socket (Node compat layer). */\n private socket: net.Socket | tls.TLSSocket | null = null;\n /** Whether the connection is currently encrypted. */\n private _secure: boolean;\n /** Whether the socket is connected. */\n private _connected = false;\n /** Socket connect timeout in milliseconds. */\n private readonly connectionTimeout: number;\n /** TLS options for direct TLS and STARTTLS upgrades. */\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 /** Opens a plain TCP connection to the SMTP server. */\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 /** Opens a direct TLS connection to the SMTP server. */\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
7
|
"mappings": "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",
|
|
8
8
|
"debugId": "94AAFD676203266764756E2164756E21",
|
package/dist/adapters/cf.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* @example
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { CloudflareAdapter } from "sently/adapters/cf";
|
|
8
|
-
* import {
|
|
8
|
+
* import { createSMTPMailer } from "sently/smtp";
|
|
9
9
|
*
|
|
10
|
-
* const mailer = await
|
|
10
|
+
* const mailer = await createSMTPMailer({
|
|
11
11
|
* host: "smtp.example.com",
|
|
12
12
|
* adapter: new CloudflareAdapter(),
|
|
13
13
|
* auth: { user: "relay@example.com", pass: "secret" },
|
package/dist/adapters/cf.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{W as q}from"../chunk-keksa6fx.js";class w{socket=null;writer=null;_secure;_connected=!1;directTls;starttls;constructor(b={}){this._secure=b.secure??!1,this.directTls=b.secure??!1,this.starttls=b.starttls??!this.directTls}get secure(){return this._secure}get connected(){return this._connected}async connect(b,g){let{connect:j}=await import("cloudflare:sockets"),m=this.directTls?"on":this.starttls?"starttls":"off";this.socket=j({hostname:b,port:g},{secureTransport:m}),this.writer=this.socket.writable.getWriter(),this._connected=!0,this._secure=m==="on"}async startTLS(b){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");await this.writer?.close(),this.socket=this.socket.startTls(),this.writer=this.socket.writable.getWriter(),this._secure=!0}async write(b){if(!this.writer)throw Error("Socket not connected");await this.writer.write(b)}async*read(){if(!this.socket)throw Error("Socket not connected");let b=this.socket.readable.getReader();try{while(!0){let{value:g,done:j}=await b.read();if(j)break;if(g)yield g}}finally{b.releaseLock()}}async close(){await this.writer?.close(),await this.socket?.close(),this.writer=null,this.socket=null,this._connected=!1}}export{w as CloudflareAdapter};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=7B1527506618792564756E2164756E21
|
package/dist/adapters/cf.js.map
CHANGED
|
@@ -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 {
|
|
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 { createSMTPMailer } from \"sently/smtp\";\n *\n * const mailer = await createSMTPMailer({\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 /** Underlying Cloudflare Workers socket. */\n private socket: CFSocket | null = null;\n /** Writable stream writer for outbound bytes. */\n private writer: WritableStreamDefaultWriter<Uint8Array> | null = null;\n /** Whether the connection is currently encrypted. */\n private _secure: boolean;\n /** Whether the socket is connected. */\n private _connected = false;\n /** Whether implicit TLS is used on connect (port 465 style). */\n private readonly directTls: boolean;\n /** Whether STARTTLS upgrade is enabled after plain connect. */\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
7
|
"mappings": "yCAiDO,MAAM,CAA2C,CAE9C,OAA0B,KAE1B,OAAyD,KAEzD,QAEA,WAAa,GAEJ,UAEA,SAGjB,WAAW,CAAC,EAAoC,CAAC,EAAG,CAClD,KAAK,QAAU,EAAQ,QAAU,GACjC,KAAK,UAAY,EAAQ,QAAU,GACnC,KAAK,SAAW,EAAQ,UAAY,CAAC,KAAK,aAIxC,OAAM,EAAY,CACpB,OAAO,KAAK,WAIV,UAAS,EAAY,CACvB,OAAO,KAAK,gBAIR,QAAO,CAAC,EAAc,EAA6B,CACvD,IAAQ,WAAa,KAAa,8BAE5B,EAAkB,KAAK,UAAY,KAAO,KAAK,SAAW,WAAa,MAC7E,KAAK,OAAS,EAAQ,CAAE,SAAU,EAAM,MAAK,EAAG,CAAE,iBAAgB,CAAC,EACnE,KAAK,OAAS,KAAK,OAAO,SAAS,UAAU,EAC7C,KAAK,WAAa,GAClB,KAAK,QAAU,IAAoB,UAI/B,SAAQ,CAAC,EAAsC,CACnD,GAAI,CAAC,KAAK,QAAU,KAAK,QACvB,MAAU,MAAM,4CAA4C,EAG9D,MAAM,KAAK,QAAQ,MAAM,EACzB,KAAK,OAAS,KAAK,OAAO,SAAS,EACnC,KAAK,OAAS,KAAK,OAAO,SAAS,UAAU,EAC7C,KAAK,QAAU,QAIX,MAAK,CAAC,EAAiC,CAC3C,GAAI,CAAC,KAAK,OACR,MAAU,MAAM,sBAAsB,EAExC,MAAM,KAAK,OAAO,MAAM,CAAI,QAIvB,IAAI,EAA8C,CACvD,GAAI,CAAC,KAAK,OACR,MAAU,MAAM,sBAAsB,EAGxC,IAAM,EAAS,KAAK,OAAO,SAAS,UAAU,EAC9C,GAAI,CACF,MAAO,GAAM,CACX,IAAQ,QAAO,QAAS,MAAM,EAAO,KAAK,EAC1C,GAAI,EACF,MAEF,GAAI,EACF,MAAM,UAGV,CACA,EAAO,YAAY,QAKjB,MAAK,EAAkB,CAC3B,MAAM,KAAK,QAAQ,MAAM,EACzB,MAAM,KAAK,QAAQ,MAAM,EACzB,KAAK,OAAS,KACd,KAAK,OAAS,KACd,KAAK,WAAa,GAEtB",
|
|
8
|
-
"debugId": "
|
|
8
|
+
"debugId": "7B1527506618792564756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
package/dist/adapters/deno.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* @example
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { DenoAdapter } from "sently/adapters/deno";
|
|
8
|
-
* import {
|
|
8
|
+
* import { createSMTPMailer } from "sently/smtp";
|
|
9
9
|
*
|
|
10
|
-
* const mailer = await
|
|
10
|
+
* const mailer = await createSMTPMailer({
|
|
11
11
|
* host: "smtp.example.com",
|
|
12
12
|
* adapter: new DenoAdapter(),
|
|
13
13
|
* auth: { user: "you@example.com", pass: "secret" },
|
package/dist/adapters/deno.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import"../chunk-
|
|
1
|
+
import"../chunk-keksa6fx.js";class v{conn=null;_secure;_connected=!1;tlsOptions;constructor(j={}){if(typeof Deno>"u")throw Error("DenoAdapter requires the Deno runtime");this._secure=j.secure??!1,this.tlsOptions=j.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(j,q){if(this._secure)this.conn=await Deno.connectTls({hostname:j,port:q,...this.tlsOptions.servername?{servername:this.tlsOptions.servername}:{}});else this.conn=await Deno.connect({hostname:j,port:q});this._connected=!0}async startTLS(j){if(!this.conn||this._secure)throw Error("Cannot STARTTLS: no plain connection available");let q={...this.tlsOptions,...j};this.conn=await Deno.startTls(this.conn,{...q.servername?{hostname:q.servername}:{}}),this._secure=!0}async write(j){if(!this.conn)throw Error("Socket not connected");await this.conn.write(j)}async*read(){if(!this.conn)throw Error("Socket not connected");let j=new Uint8Array(8192);while(!0){let q=await this.conn.read(j);if(q===null)break;yield j.slice(0,q)}}async close(){this.conn?.close(),this.conn=null,this._connected=!1}}export{v as DenoAdapter};
|
|
2
2
|
|
|
3
3
|
//# debugId=433F0358DDA6910C64756E2164756E21
|
|
@@ -2,7 +2,7 @@
|
|
|
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 {
|
|
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 { createSMTPMailer } from \"sently/smtp\";\n *\n * const mailer = await createSMTPMailer({\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 /** Active Deno TCP or TLS connection. */\n private conn: DenoConn | null = null;\n /** Whether the connection is currently encrypted. */\n private _secure: boolean;\n /** Whether the socket is connected. */\n private _connected = false;\n /** TLS options for direct TLS and STARTTLS upgrades. */\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
7
|
"mappings": "6BAkDO,MAAM,CAAqC,CAExC,KAAwB,KAExB,QAEA,WAAa,GAEJ,WAGjB,WAAW,CAAC,EAA8B,CAAC,EAAG,CAC5C,GAAI,OAAO,KAAS,IAClB,MAAU,MAAM,uCAAuC,EAEzD,KAAK,QAAU,EAAQ,QAAU,GACjC,KAAK,WAAa,EAAQ,KAAO,CAAC,KAIhC,OAAM,EAAY,CACpB,OAAO,KAAK,WAIV,UAAS,EAAY,CACvB,OAAO,KAAK,gBAIR,QAAO,CAAC,EAAc,EAA6B,CACvD,GAAI,KAAK,QACP,KAAK,KAAO,MAAM,KAAK,WAAW,CAChC,SAAU,EACV,UACI,KAAK,WAAW,WAAa,CAAE,WAAY,KAAK,WAAW,UAAW,EAAI,CAAC,CACjF,CAAC,EAED,UAAK,KAAO,MAAM,KAAK,QAAQ,CAAE,SAAU,EAAM,MAAK,CAAC,EAEzD,KAAK,WAAa,QAId,SAAQ,CAAC,EAAqC,CAClD,GAAI,CAAC,KAAK,MAAQ,KAAK,QACrB,MAAU,MAAM,gDAAgD,EAGlE,IAAM,EAAS,IAAK,KAAK,cAAe,CAAQ,EAChD,KAAK,KAAO,MAAM,KAAK,SAAS,KAAK,KAAqB,IACpD,EAAO,WAAa,CAAE,SAAU,EAAO,UAAW,EAAI,CAAC,CAC7D,CAAC,EACD,KAAK,QAAU,QAIX,MAAK,CAAC,EAAiC,CAC3C,GAAI,CAAC,KAAK,KACR,MAAU,MAAM,sBAAsB,EAExC,MAAM,KAAK,KAAK,MAAM,CAAI,QAIrB,IAAI,EAA8C,CACvD,GAAI,CAAC,KAAK,KACR,MAAU,MAAM,sBAAsB,EAGxC,IAAM,EAAS,IAAI,WAAW,IAAI,EAClC,MAAO,GAAM,CACX,IAAM,EAAI,MAAM,KAAK,KAAK,KAAK,CAAM,EACrC,GAAI,IAAM,KACR,MAEF,MAAM,EAAO,MAAM,EAAG,CAAC,QAKrB,MAAK,EAAkB,CAC3B,KAAK,MAAM,MAAM,EACjB,KAAK,KAAO,KACZ,KAAK,WAAa,GAEtB",
|
|
8
8
|
"debugId": "433F0358DDA6910C64756E2164756E21",
|
package/dist/adapters/node.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import"../chunk-
|
|
1
|
+
import"../chunk-keksa6fx.js";import W from"node:net";import Q from"node:tls";function V(f){if(f.rejectUnauthorized===!1)console.warn("[sently] TLS certificate verification is disabled. Never use rejectUnauthorized: false in production.")}class X{socket=null;_secure;_connected=!1;connectionTimeout;tlsOptions;constructor(f={}){this._secure=f.secure??!1,this.connectionTimeout=f.connectionTimeout??30000,this.tlsOptions=f.tls??{}}get secure(){return this._secure}get connected(){return this._connected}async connect(f,B){if(this._secure)await this.connectTls(f,B);else await this.connectPlain(f,B);this._connected=!0}async startTLS(f){if(!this.socket||this._secure)throw Error("Cannot STARTTLS: no plain socket available");let B=this.socket,y={...this.tlsOptions,...f};V(y),await new Promise((F,A)=>{let G=Q.connect({socket:B,servername:y.servername,rejectUnauthorized:y.rejectUnauthorized??!0,minVersion:y.minVersion});G.once("secureConnect",()=>{this.socket=G,this._secure=!0,F()}),G.once("error",A)})}async write(f){if(!this.socket)throw Error("Socket not connected");await new Promise((B,y)=>{this.socket?.write(Buffer.from(f),(F)=>{if(F)y(F);else B()})})}async*read(){if(!this.socket)throw Error("Socket not connected");let f=this.socket,B=[],y=null,F=!1,A=null,G=(H)=>{let I=new Uint8Array(H);if(y)y({value:I,done:!1}),y=null;else B.push(I)},J=(H)=>{if(A=H,y)y({value:void 0,done:!0}),y=null;F=!0},K=()=>{if(F=!0,y)y({value:void 0,done:!0}),y=null};f.on("data",G),f.on("error",J),f.on("close",K);try{while(!F||B.length>0){if(A)throw A;if(B.length>0){yield B.shift();continue}if(F)break;let H=await new Promise((I)=>{y=I});if(H.done)break;yield H.value}}finally{f.off("data",G),f.off("error",J),f.off("close",K)}}async close(){if(!this.socket)return;await new Promise((f)=>{this.socket?.end(()=>f())}),this.socket=null,this._connected=!1}connectPlain(f,B){return new Promise((y,F)=>{let A=W.connect({host:f,port:B},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}connectTls(f,B){return V(this.tlsOptions),new Promise((y,F)=>{let A=Q.connect({host:f,port:B,servername:this.tlsOptions.servername??f,rejectUnauthorized:this.tlsOptions.rejectUnauthorized??!0,minVersion:this.tlsOptions.minVersion},()=>y());A.setTimeout(this.connectionTimeout),A.once("timeout",()=>{A.destroy(),F(Error("Connection timeout"))}),A.once("error",F),this.socket=A})}}export{X as NodeAdapter};
|
|
2
2
|
|
|
3
3
|
//# debugId=81CB75805F76C9AC64756E2164756E21
|