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.
Files changed (96) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +86 -98
  3. package/dist/adapters/bun.js +1 -1
  4. package/dist/adapters/bun.js.map +1 -1
  5. package/dist/adapters/cf.d.ts +2 -2
  6. package/dist/adapters/cf.js +2 -2
  7. package/dist/adapters/cf.js.map +2 -2
  8. package/dist/adapters/deno.d.ts +2 -2
  9. package/dist/adapters/deno.js +1 -1
  10. package/dist/adapters/deno.js.map +1 -1
  11. package/dist/adapters/node.js +1 -1
  12. package/dist/adapters/node.js.map +1 -1
  13. package/dist/auth/oauth2.js +1 -1
  14. package/dist/{chunk-qnz9z0d4.js → chunk-4mvp3b85.js} +2 -2
  15. package/dist/chunk-4mvp3b85.js.map +10 -0
  16. package/dist/chunk-74rha3ac.js +4 -0
  17. package/dist/chunk-74rha3ac.js.map +10 -0
  18. package/dist/{chunk-n601jhpd.js → chunk-a1c87hn1.js} +2 -2
  19. package/dist/chunk-a1c87hn1.js.map +10 -0
  20. package/dist/chunk-cvrbfke9.js +13 -0
  21. package/dist/{chunk-w3fnjh04.js.map → chunk-cvrbfke9.js.map} +2 -2
  22. package/dist/{chunk-wzdcz88x.js → chunk-d5rwm3br.js} +2 -2
  23. package/dist/{chunk-wzdcz88x.js.map → chunk-d5rwm3br.js.map} +1 -1
  24. package/dist/{chunk-y4qxdyqn.js → chunk-gh5n759p.js} +3 -3
  25. package/dist/{chunk-y4qxdyqn.js.map → chunk-gh5n759p.js.map} +1 -1
  26. package/dist/{chunk-y72qggqf.js → chunk-gkkv7yqm.js} +2 -2
  27. package/dist/{chunk-y72qggqf.js.map → chunk-gkkv7yqm.js.map} +1 -1
  28. package/dist/chunk-ka6t8g77.js +4 -0
  29. package/dist/chunk-ka6t8g77.js.map +11 -0
  30. package/dist/{chunk-tfxje74k.js → chunk-kebhy657.js} +2 -2
  31. package/dist/{chunk-tfxje74k.js.map → chunk-kebhy657.js.map} +1 -1
  32. package/dist/{chunk-rszb1n1d.js → chunk-keksa6fx.js} +2 -2
  33. package/dist/{chunk-rszb1n1d.js.map → chunk-keksa6fx.js.map} +1 -1
  34. package/dist/{chunk-z7j2n9k8.js → chunk-r7ee9tk7.js} +2 -2
  35. package/dist/{chunk-z7j2n9k8.js.map → chunk-r7ee9tk7.js.map} +1 -1
  36. package/dist/{chunk-348wndn3.js → chunk-rm78s1ek.js} +2 -2
  37. package/dist/{chunk-348wndn3.js.map → chunk-rm78s1ek.js.map} +1 -1
  38. package/dist/core/errors.d.ts +1 -1
  39. package/dist/core/errors.js +2 -2
  40. package/dist/core/errors.js.map +1 -1
  41. package/dist/core/smtp.js +2 -2
  42. package/dist/core/smtp.js.map +1 -1
  43. package/dist/core/types.d.ts +4 -8
  44. package/dist/detect.d.ts +1 -7
  45. package/dist/detect.js +2 -2
  46. package/dist/detect.js.map +3 -4
  47. package/dist/dkim.js +2 -2
  48. package/dist/dkim.js.map +1 -1
  49. package/dist/idempotency.js +2 -2
  50. package/dist/idempotency.js.map +2 -2
  51. package/dist/index.d.ts +12 -98
  52. package/dist/index.js +4 -98
  53. package/dist/mailer.js +2 -2
  54. package/dist/mailer.js.map +1 -1
  55. package/dist/plugins/react.js +2 -2
  56. package/dist/plugins/react.js.map +1 -1
  57. package/dist/plugins/template.js +1 -1
  58. package/dist/pool/pool.js +2 -2
  59. package/dist/pool/pool.js.map +1 -1
  60. package/dist/smtp-mailer.d.ts +23 -0
  61. package/dist/smtp-mailer.js +3 -0
  62. package/dist/smtp-mailer.js.map +10 -0
  63. package/dist/transports/brevo.js +2 -2
  64. package/dist/transports/brevo.js.map +2 -2
  65. package/dist/transports/mailgun.js +2 -2
  66. package/dist/transports/mailgun.js.map +3 -3
  67. package/dist/transports/postmark.js +2 -2
  68. package/dist/transports/postmark.js.map +3 -3
  69. package/dist/transports/preview.js +2 -2
  70. package/dist/transports/preview.js.map +1 -1
  71. package/dist/transports/resend.js +2 -2
  72. package/dist/transports/resend.js.map +2 -2
  73. package/dist/transports/retry.js +2 -2
  74. package/dist/transports/retry.js.map +4 -3
  75. package/dist/transports/sendgrid.js +2 -2
  76. package/dist/transports/sendgrid.js.map +2 -2
  77. package/dist/transports/ses.js +2 -2
  78. package/dist/transports/ses.js.map +2 -2
  79. package/dist/transports/smtp.d.ts +1 -0
  80. package/dist/transports/smtp.js +2 -2
  81. package/dist/transports/smtp.js.map +1 -1
  82. package/dist/webhooks/brevo.js +1 -1
  83. package/dist/webhooks/mailgun.js +1 -1
  84. package/dist/webhooks/postmark.js +1 -1
  85. package/dist/webhooks/resend.js +1 -1
  86. package/dist/webhooks/sendgrid.js +1 -1
  87. package/dist/webhooks/ses.js +1 -1
  88. package/dist/webhooks/timing-safe-equal.js +1 -1
  89. package/package.json +5 -1
  90. package/dist/chunk-3y1tr1vt.js +0 -4
  91. package/dist/chunk-3y1tr1vt.js.map +0 -11
  92. package/dist/chunk-n601jhpd.js.map +0 -10
  93. package/dist/chunk-qnz9z0d4.js.map +0 -10
  94. package/dist/chunk-t0en64h9.js +0 -4
  95. package/dist/chunk-t0en64h9.js.map +0 -10
  96. 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 ~5.4 KB via `sently/mailer`.
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
  [![tests](https://img.shields.io/badge/tests-passing-brightgreen)](#)
15
15
  [![GitHub](https://img.shields.io/github/stars/alialnaghmoush/sently?style=social&label=GitHub)](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.6 KB HTTP · ~15 KB SMTP |
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 { createMailer as createSMTPMailer } from "sently"; // SMTP host/port config
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 { createMailer } from "sently";
114
+ import { createSMTPMailer } from "sently/smtp";
112
115
 
113
- const mailer = await createMailer({
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 { createMailer } from "sently";
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 createMailer({
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) | `createMailer(config)` | Auto-detected |
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) | `createMailer(config)` | Auto-detected |
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 createMailer({
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
- Use `sently/mailer` instead of `sently` when passing `{ transport }` keeps HTTP-only bundles ~10 KB smaller.
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
- const mailer = await createMailer({
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 { OAuth2Client } from "sently/auth/oauth2";
251
+ import { createSMTPMailer } from "sently/smtp";
246
252
 
247
- const mailer = await createMailer({
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 uses the same `OAuth2Client` — pass `tokenUrl: MICROSOFT_TOKEN_URL` (from `sently/auth/oauth2`) on the `oauth2` config.
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
- const mailer = await createMailer({
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 { createMailer, type MailOptions } from "sently";
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 createMailer({
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, SMTPError } from "sently";
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 Size
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
- **Nodemailer ships ~220 KB** whether you use SMTP or an HTTP plugin. sently tree-shakes per subpath.
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
- ### Choosing an import path
704
+ Nodemailer ships **~220 KB** regardless of transport. sently tree-shakes by subpath — pick the entry that matches how you send:
677
705
 
678
- | You send via… | Import | Why |
679
- |---------------|--------|-----|
680
- | Resend, SendGrid, Postmark, etc. | `sently/mailer` + `sently/transports/<provider>` | **~6.6 KB** — no SMTP code in the bundle |
681
- | SMTP relay (`host` / `port`) | `sently` | **~15 KB** — includes MIME + SMTP stack |
682
- | Raw transport, no plugins | `sently/transports/<provider>` only | **~4.7 KB** — skip `createMailer` wrapper |
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
- // Recommended — HTTP API (~6.6 KB bundled)
713
+ // HTTP
691
714
  import { createMailer } from "sently/mailer";
692
715
  import { ResendTransport } from "sently/transports/resend";
693
716
 
694
- // Avoid for HTTP-only apps — pulls SMTP into flat bundles (~16 KB)
695
- import { createMailer } from "sently";
717
+ // SMTP
718
+ import { createSMTPMailer } from "sently/smtp";
696
719
  ```
697
720
 
698
- ### Common stacks
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
- ### Core entries
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
- ### What's inside an HTTP stack (~6.6 KB)
725
+ ## Choosing an entrypoint
748
726
 
749
727
  ```
750
- sently/mailer 3.1 KB createMailer wrapper, plugins, hooks, sendBulk + rate limit
751
- transports/resend 4.7 KB fetch client + MIME/address parsing + batch
752
- ─────
753
- total 6.6 KB vs Nodemailer ~220 KB
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 createMailer({...})` |
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'` (SMTP) |
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
 
@@ -1,3 +1,3 @@
1
- import"../chunk-rszb1n1d.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};
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
@@ -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 { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new BunAdapter(),\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport net from \"node:net\";\nimport tls from \"node:tls\";\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\nfunction warnRejectUnauthorizedDisabled(tls: TLSOptions): void {\n if (tls.rejectUnauthorized === false) {\n console.warn(\n \"[sently] TLS certificate verification is disabled. \" +\n \"Never use rejectUnauthorized: false in production.\",\n );\n }\n}\n\ndeclare const Bun: { version: string } | undefined;\n\n/** Configuration options for {@link BunAdapter}. */\nexport interface BunAdapterOptions {\n /** Use implicit TLS on connect (port 465). Default: false. */\n secure?: boolean;\n /** Socket connect timeout in milliseconds. Default: 30_000. */\n connectionTimeout?: number;\n /** TLS options passed to node:tls (Bun Node compat layer). */\n tls?: TLSOptions;\n}\n\n/**\n * Bun socket adapter using node:net and node:tls (Node compat layer).\n */\nexport class BunAdapter implements SocketAdapter {\n /** 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"
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": "6BAgBA,wBACA,wBAGA,SAAS,CAA8B,CAAC,EAAuB,CAC7D,GAAI,EAAI,qBAAuB,GAC7B,QAAQ,KACN,uGAEF,EAmBG,MAAM,CAAoC,CAEvC,OAA4C,KAE5C,QAEA,WAAa,GAEJ,kBAEA,WAGjB,WAAW,CAAC,EAA6B,CAAC,EAAG,CAC3C,GAAI,OAAO,IAAQ,IACjB,MAAU,MAAM,qCAAqC,EAEvD,KAAK,QAAU,EAAQ,QAAU,GACjC,KAAK,kBAAoB,EAAQ,mBAAqB,MACtD,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,MAAM,KAAK,WAAW,EAAM,CAAI,EAEhC,WAAM,KAAK,aAAa,EAAM,CAAI,EAEpC,KAAK,WAAa,QAId,SAAQ,CAAC,EAAqC,CAClD,GAAI,CAAC,KAAK,QAAU,KAAK,QACvB,MAAU,MAAM,4CAA4C,EAG9D,IAAM,EAAQ,KAAK,OACb,EAAS,IAAK,KAAK,cAAe,CAAQ,EAChD,EAA+B,CAAM,EAErC,MAAM,IAAI,QAAc,CAAC,EAAS,IAAW,CAC3C,IAAM,EAAY,EAAI,QAAQ,CAC5B,OAAQ,EACR,WAAY,EAAO,WACnB,mBAAoB,EAAO,oBAAsB,GACjD,WAAY,EAAO,UACrB,CAAC,EAED,EAAU,KAAK,gBAAiB,IAAM,CACpC,KAAK,OAAS,EACd,KAAK,QAAU,GACf,EAAQ,EACT,EACD,EAAU,KAAK,QAAS,CAAM,EAC/B,OAIG,MAAK,CAAC,EAAiC,CAC3C,GAAI,CAAC,KAAK,OACR,MAAU,MAAM,sBAAsB,EAGxC,MAAM,IAAI,QAAc,CAAC,EAAS,IAAW,CAC3C,KAAK,QAAQ,MAAM,OAAO,KAAK,CAAI,EAAG,CAAC,IAAkC,CACvE,GAAI,EACF,EAAO,CAAG,EAEV,OAAQ,EAEX,EACF,QAII,IAAI,EAA8C,CACvD,GAAI,CAAC,KAAK,OACR,MAAU,MAAM,sBAAsB,EAGxC,IAAM,EAAS,KAAK,OACd,EAAsB,CAAC,EACzB,EAAoE,KACpE,EAAO,GACP,EAAsB,KAEpB,EAAS,CAAC,IAAwB,CACtC,IAAM,EAAO,IAAI,WAAW,CAAK,EACjC,GAAI,EACF,EAAY,CAAE,MAAO,EAAM,KAAM,EAAM,CAAC,EACxC,EAAc,KAEd,OAAM,KAAK,CAAI,GAIb,EAAU,CAAC,IAAqB,CAGpC,GAFA,EAAQ,EACR,EAAO,GACH,EACF,EAAY,CAAE,MAAO,OAAoC,KAAM,EAAK,CAAC,EACrE,EAAc,MAIZ,EAAU,IAAY,CAE1B,GADA,EAAO,GACH,EACF,EAAY,CAAE,MAAO,OAAoC,KAAM,EAAK,CAAC,EACrE,EAAc,MAIlB,EAAO,GAAG,OAAQ,CAAM,EACxB,EAAO,GAAG,QAAS,CAAO,EAC1B,EAAO,GAAG,QAAS,CAAO,EAE1B,GAAI,CACF,MAAO,CAAC,GAAQ,EAAM,OAAS,EAAG,CAChC,GAAI,EACF,MAAM,EAER,GAAI,EAAM,OAAS,EAAG,CACpB,MAAM,EAAM,MAAM,EAClB,SAEF,GAAI,EACF,MAEF,IAAM,EAAQ,MAAM,IAAI,QAAoC,CAAC,IAAY,CACvE,EAAc,EACf,EACD,GAAI,EAAM,KACR,MAEF,MAAM,EAAM,cAEd,CACA,EAAO,IAAI,OAAQ,CAAM,EACzB,EAAO,IAAI,QAAS,CAAO,EAC3B,EAAO,IAAI,QAAS,CAAO,QAKzB,MAAK,EAAkB,CAC3B,GAAI,CAAC,KAAK,OACR,OAGF,MAAM,IAAI,QAAc,CAAC,IAAY,CACnC,KAAK,QAAQ,IAAI,IAAM,EAAQ,CAAC,EACjC,EACD,KAAK,OAAS,KACd,KAAK,WAAa,GAIZ,YAAY,CAAC,EAAc,EAA6B,CAC9D,OAAO,IAAI,QAAQ,CAAC,EAAS,IAAW,CACtC,IAAM,EAAS,EAAI,QAAQ,CAAE,OAAM,MAAK,EAAG,IAAM,EAAQ,CAAC,EAC1D,EAAO,WAAW,KAAK,iBAAiB,EACxC,EAAO,KAAK,UAAW,IAAM,CAC3B,EAAO,QAAQ,EACf,EAAW,MAAM,oBAAoB,CAAC,EACvC,EACD,EAAO,KAAK,QAAS,CAAM,EAC3B,KAAK,OAAS,EACf,EAIK,UAAU,CAAC,EAAc,EAA6B,CAE5D,OADA,EAA+B,KAAK,UAAU,EACvC,IAAI,QAAQ,CAAC,EAAS,IAAW,CACtC,IAAM,EAAS,EAAI,QACjB,CACE,OACA,OACA,WAAY,KAAK,WAAW,YAAc,EAC1C,mBAAoB,KAAK,WAAW,oBAAsB,GAC1D,WAAY,KAAK,WAAW,UAC9B,EACA,IAAM,EAAQ,CAChB,EACA,EAAO,WAAW,KAAK,iBAAiB,EACxC,EAAO,KAAK,UAAW,IAAM,CAC3B,EAAO,QAAQ,EACf,EAAW,MAAM,oBAAoB,CAAC,EACvC,EACD,EAAO,KAAK,QAAS,CAAM,EAC3B,KAAK,OAAS,EACf,EAEL",
8
8
  "debugId": "94AAFD676203266764756E2164756E21",
@@ -5,9 +5,9 @@
5
5
  * @example
6
6
  * ```ts
7
7
  * import { CloudflareAdapter } from "sently/adapters/cf";
8
- * import { createMailer } from "sently";
8
+ * import { createSMTPMailer } from "sently/smtp";
9
9
  *
10
- * const mailer = await createMailer({
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" },
@@ -1,3 +1,3 @@
1
- import{V as q}from"../chunk-rszb1n1d.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};
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=0AEF1D7948DF0E9B64756E2164756E21
3
+ //# debugId=7B1527506618792564756E2164756E21
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/adapters/cf.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @module\n * Cloudflare Workers socket adapter for SMTP via cloudflare:sockets.\n *\n * @example\n * ```ts\n * import { CloudflareAdapter } from \"sently/adapters/cf\";\n * import { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new CloudflareAdapter(),\n * auth: { user: \"relay@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\ninterface CFSocket {\n readonly readable: ReadableStream<Uint8Array>;\n readonly writable: WritableStream<Uint8Array>;\n readonly closed: Promise<void>;\n close(): Promise<void>;\n startTls(): CFSocket;\n}\n\ntype CFConnect = (\n address: { hostname: string; port: number },\n options?: { secureTransport?: \"off\" | \"on\" | \"starttls\"; allowHalfOpen?: boolean },\n) => CFSocket;\n\n/** Configuration options for {@link CloudflareAdapter}. */\nexport interface CloudflareAdapterOptions {\n /** 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"
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": "0AEF1D7948DF0E9B64756E2164756E21",
8
+ "debugId": "7B1527506618792564756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -5,9 +5,9 @@
5
5
  * @example
6
6
  * ```ts
7
7
  * import { DenoAdapter } from "sently/adapters/deno";
8
- * import { createMailer } from "sently";
8
+ * import { createSMTPMailer } from "sently/smtp";
9
9
  *
10
- * const mailer = await createMailer({
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" },
@@ -1,3 +1,3 @@
1
- import"../chunk-rszb1n1d.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};
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 { createMailer } from \"sently\";\n *\n * const mailer = await createMailer({\n * host: \"smtp.example.com\",\n * adapter: new DenoAdapter(),\n * auth: { user: \"you@example.com\", pass: \"secret\" },\n * });\n * ```\n */\nimport type { SocketAdapter, TLSOptions } from \"../core/types.js\";\n\ndeclare const Deno: {\n connect(options: { hostname: string; port: number }): Promise<DenoTcpConn>;\n connectTls(options: {\n hostname: string;\n port: number;\n [key: string]: unknown;\n }): Promise<DenoTlsConn>;\n startTls(conn: DenoTcpConn, options?: { hostname?: string }): Promise<DenoTlsConn>;\n};\n\ninterface DenoConn {\n read(p: Uint8Array): Promise<number | null>;\n write(p: Uint8Array): Promise<number>;\n close(): void;\n}\n\ninterface DenoTcpConn extends DenoConn {}\ninterface DenoTlsConn extends DenoConn {}\n\n/** Configuration options for {@link DenoAdapter}. */\nexport interface DenoAdapterOptions {\n /** Use implicit TLS on connect (port 465). Default: false. */\n secure?: boolean;\n /** Socket connect timeout in milliseconds. Default: 30_000. */\n connectionTimeout?: number;\n /** TLS options passed to Deno.startTls / Deno.connectTls. */\n tls?: TLSOptions;\n}\n\n/**\n * Deno socket adapter using Deno.connect / Deno.startTls.\n */\nexport class DenoAdapter implements SocketAdapter {\n /** 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"
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",
@@ -1,3 +1,3 @@
1
- import"../chunk-rszb1n1d.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};
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