sently 0.8.0 → 0.9.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/AGENTS.md +34 -0
- package/CHANGELOG.md +33 -1
- package/README.md +89 -960
- package/dist/adapters/bun.js +1 -1
- package/dist/adapters/cf.js +2 -2
- package/dist/adapters/cf.js.map +2 -2
- package/dist/adapters/deno.js +1 -1
- package/dist/adapters/node.js +1 -1
- package/dist/auth/oauth2.js +2 -2
- package/dist/auth/oauth2.js.map +1 -1
- package/dist/chunk-0gqy32pe.js +4 -0
- package/dist/{chunk-n5dan5bz.js.map → chunk-0gqy32pe.js.map} +2 -2
- package/dist/{chunk-z62q7kqr.js → chunk-0qxws3kj.js} +2 -2
- package/dist/{chunk-z62q7kqr.js.map → chunk-0qxws3kj.js.map} +1 -1
- package/dist/chunk-29z7fkzy.js +4 -0
- package/dist/{chunk-wsymfbkn.js.map → chunk-29z7fkzy.js.map} +2 -2
- package/dist/chunk-5915vbcj.js +4 -0
- package/dist/chunk-5915vbcj.js.map +10 -0
- package/dist/{chunk-nbmq8ejv.js → chunk-6npp3x3c.js} +2 -2
- package/dist/{chunk-nbmq8ejv.js.map → chunk-6npp3x3c.js.map} +1 -1
- package/dist/chunk-7gy2q9sh.js +4 -0
- package/dist/chunk-7gy2q9sh.js.map +10 -0
- package/dist/{chunk-zst8an2p.js → chunk-8dty8c4p.js} +2 -2
- package/dist/{chunk-zst8an2p.js.map → chunk-8dty8c4p.js.map} +2 -2
- package/dist/{chunk-f3dz43cy.js → chunk-8njyga62.js} +2 -2
- package/dist/{chunk-f3dz43cy.js.map → chunk-8njyga62.js.map} +1 -1
- package/dist/chunk-93vqxxj2.js +4 -0
- package/dist/{chunk-qh90yr6d.js.map → chunk-93vqxxj2.js.map} +2 -2
- package/dist/chunk-b602dhck.js +4 -0
- package/dist/{chunk-wbvhaqcp.js.map → chunk-b602dhck.js.map} +2 -2
- package/dist/{chunk-9zkv5azm.js → chunk-gp4fwrs8.js} +2 -2
- package/dist/{chunk-9zkv5azm.js.map → chunk-gp4fwrs8.js.map} +1 -1
- package/dist/chunk-n0qeyzqm.js +13 -0
- package/dist/{chunk-8phpsns1.js.map → chunk-n0qeyzqm.js.map} +2 -2
- package/dist/chunk-qtkd5bak.js +4 -0
- package/dist/chunk-qtkd5bak.js.map +11 -0
- package/dist/chunk-skqhj0rm.js +4 -0
- package/dist/{chunk-vhxzhx8b.js.map → chunk-skqhj0rm.js.map} +2 -2
- package/dist/{chunk-6bkj4y4r.js → chunk-tamww15j.js} +2 -2
- package/dist/{chunk-6bkj4y4r.js.map → chunk-tamww15j.js.map} +1 -1
- package/dist/chunk-vjpds2pf.js +3 -0
- package/dist/{chunk-m07smapm.js.map → chunk-vjpds2pf.js.map} +2 -2
- package/dist/chunk-yp311efr.js +4 -0
- package/dist/chunk-yp311efr.js.map +10 -0
- package/dist/chunk-ywq6s10g.js +5 -0
- package/dist/{chunk-5scdgffb.js.map → chunk-ywq6s10g.js.map} +3 -3
- package/dist/core/base64.d.ts +10 -0
- package/dist/core/errors.js +2 -2
- package/dist/core/errors.js.map +1 -1
- package/dist/core/hooks.d.ts +14 -0
- package/dist/core/plugin.d.ts +7 -7
- package/dist/core/push-endpoint.d.ts +29 -0
- package/dist/core/push-types.d.ts +98 -0
- package/dist/core/sms-types.d.ts +76 -0
- package/dist/core/smtp.js +2 -2
- package/dist/core/smtp.js.map +1 -1
- package/dist/core/whatsapp-types.d.ts +116 -0
- package/dist/detect.js +2 -2
- package/dist/detect.js.map +1 -1
- package/dist/dkim.js +2 -2
- package/dist/dkim.js.map +2 -2
- package/dist/idempotency.js +2 -2
- package/dist/idempotency.js.map +1 -1
- package/dist/index.d.ts +5 -32
- package/dist/index.js +0 -35
- package/dist/mailer.js +2 -2
- package/dist/mailer.js.map +1 -1
- package/dist/observability/console.js +1 -1
- package/dist/plugins/react.js +2 -2
- package/dist/plugins/react.js.map +2 -2
- package/dist/plugins/template.js +1 -1
- package/dist/pool/pool.js +2 -2
- package/dist/pool/pool.js.map +1 -1
- package/dist/push.d.ts +26 -0
- package/dist/push.js +3 -0
- package/dist/push.js.map +10 -0
- package/dist/sms.d.ts +26 -0
- package/dist/sms.js +3 -0
- package/dist/sms.js.map +10 -0
- package/dist/smtp-mailer.js +2 -2
- package/dist/smtp-mailer.js.map +2 -2
- package/dist/transports/brevo.js +2 -2
- package/dist/transports/brevo.js.map +2 -2
- package/dist/transports/cloudflare-email.js +2 -2
- package/dist/transports/cloudflare-email.js.map +2 -2
- package/dist/transports/fallback.js +2 -2
- package/dist/transports/fallback.js.map +1 -1
- package/dist/transports/loops.js +2 -2
- package/dist/transports/loops.js.map +2 -2
- package/dist/transports/mailersend.js +2 -2
- package/dist/transports/mailersend.js.map +2 -2
- package/dist/transports/mailgun.js +2 -2
- package/dist/transports/mailgun.js.map +2 -2
- package/dist/transports/mailtrap.js +2 -2
- package/dist/transports/mailtrap.js.map +2 -2
- package/dist/transports/msegat.d.ts +132 -0
- package/dist/transports/msegat.js +3 -0
- package/dist/transports/msegat.js.map +10 -0
- package/dist/transports/plunk.js +2 -2
- package/dist/transports/plunk.js.map +2 -2
- package/dist/transports/postmark.js +2 -2
- package/dist/transports/postmark.js.map +2 -2
- package/dist/transports/preview.js +2 -2
- package/dist/transports/preview.js.map +2 -2
- 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 +2 -2
- 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.js +2 -2
- package/dist/transports/smtp.js.map +1 -1
- package/dist/transports/sndr.d.ts +41 -0
- package/dist/transports/sndr.js +3 -0
- package/dist/transports/sndr.js.map +10 -0
- package/dist/transports/sparkpost.js +2 -2
- package/dist/transports/sparkpost.js.map +2 -2
- package/dist/transports/taqnyat-mail.d.ts +33 -0
- package/dist/transports/taqnyat-mail.js +3 -0
- package/dist/transports/taqnyat-mail.js.map +10 -0
- package/dist/transports/taqnyat-phone.d.ts +4 -0
- package/dist/transports/taqnyat-sms.d.ts +131 -0
- package/dist/transports/taqnyat-sms.js +3 -0
- package/dist/transports/taqnyat-sms.js.map +10 -0
- package/dist/transports/taqnyat-whatsapp.d.ts +51 -0
- package/dist/transports/taqnyat-whatsapp.js +3 -0
- package/dist/transports/taqnyat-whatsapp.js.map +10 -0
- package/dist/transports/twilio-sms.d.ts +66 -0
- package/dist/transports/twilio-sms.js +3 -0
- package/dist/transports/twilio-sms.js.map +10 -0
- package/dist/transports/webpush.d.ts +46 -0
- package/dist/transports/webpush.js +3 -0
- package/dist/transports/webpush.js.map +10 -0
- package/dist/transports/weighted-fallback.js +2 -2
- package/dist/transports/weighted-fallback.js.map +1 -1
- package/dist/transports/whatsapp-cloud.d.ts +56 -0
- package/dist/transports/whatsapp-cloud.js +3 -0
- package/dist/transports/whatsapp-cloud.js.map +10 -0
- 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/sndr.d.ts +16 -0
- package/dist/webhooks/sndr.js +3 -0
- package/dist/webhooks/sndr.js.map +10 -0
- package/dist/webhooks/timing-safe-equal.js +1 -1
- package/dist/webhooks.d.ts +9 -2
- package/dist/webhooks.js +4 -0
- package/dist/whatsapp.d.ts +26 -0
- package/dist/whatsapp.js +3 -0
- package/dist/whatsapp.js.map +10 -0
- package/package.json +85 -4
- package/site/README.md +26 -0
- package/site/content/docs/ai/index.mdx +13 -0
- package/site/content/docs/ai/llms-txt.mdx +12 -0
- package/site/content/docs/ai/mcp.mdx +16 -0
- package/site/content/docs/ai/meta.json +9 -0
- package/site/content/docs/channels/email.mdx +100 -0
- package/site/content/docs/channels/hooks.mdx +87 -0
- package/site/content/docs/channels/index.mdx +81 -0
- package/site/content/docs/channels/meta.json +12 -0
- package/site/content/docs/channels/push.mdx +92 -0
- package/site/content/docs/channels/sms.mdx +88 -0
- package/site/content/docs/channels/whatsapp.mdx +95 -0
- package/site/content/docs/decorators/fallback.mdx +34 -0
- package/site/content/docs/decorators/idempotency.mdx +34 -0
- package/site/content/docs/decorators/index.mdx +50 -0
- package/site/content/docs/decorators/meta.json +12 -0
- package/site/content/docs/decorators/preview.mdx +34 -0
- package/site/content/docs/decorators/retry.mdx +34 -0
- package/site/content/docs/decorators/weighted-fallback.mdx +34 -0
- package/site/content/docs/get-started/entrypoints.mdx +114 -0
- package/site/content/docs/get-started/index.mdx +14 -0
- package/site/content/docs/get-started/installation.mdx +78 -0
- package/site/content/docs/get-started/introduction.mdx +77 -0
- package/site/content/docs/get-started/meta.json +12 -0
- package/site/content/docs/get-started/migrate-nodemailer.mdx +23 -0
- package/site/content/docs/get-started/runtimes.mdx +17 -0
- package/site/content/docs/guides/adapters.mdx +26 -0
- package/site/content/docs/guides/attachments.mdx +19 -0
- package/site/content/docs/guides/bundle-size.mdx +78 -0
- package/site/content/docs/guides/dkim.mdx +26 -0
- package/site/content/docs/guides/index.mdx +15 -0
- package/site/content/docs/guides/meta.json +21 -0
- package/site/content/docs/guides/oauth2.mdx +23 -0
- package/site/content/docs/guides/observability.mdx +16 -0
- package/site/content/docs/guides/plugins-template.mdx +22 -0
- package/site/content/docs/guides/pool.mdx +24 -0
- package/site/content/docs/guides/react-email.mdx +20 -0
- package/site/content/docs/guides/security.mdx +17 -0
- package/site/content/docs/guides/send-bulk.mdx +18 -0
- package/site/content/docs/guides/vendor-extras-otp.mdx +18 -0
- package/site/content/docs/guides/webhooks.mdx +120 -0
- package/site/content/docs/guides/webpush-interop.mdx +18 -0
- package/site/content/docs/index.mdx +15 -0
- package/site/content/docs/meta.json +15 -0
- package/site/content/docs/quick-start/email.mdx +40 -0
- package/site/content/docs/quick-start/index.mdx +15 -0
- package/site/content/docs/quick-start/meta.json +5 -0
- package/site/content/docs/quick-start/push.mdx +36 -0
- package/site/content/docs/quick-start/sms.mdx +36 -0
- package/site/content/docs/quick-start/whatsapp.mdx +38 -0
- package/site/content/docs/reference/errors.mdx +22 -0
- package/site/content/docs/reference/exports.mdx +83 -0
- package/site/content/docs/reference/index.mdx +15 -0
- package/site/content/docs/reference/mail-options.mdx +22 -0
- package/site/content/docs/reference/meta.json +15 -0
- package/site/content/docs/reference/push-options.mdx +20 -0
- package/site/content/docs/reference/sms-options.mdx +17 -0
- package/site/content/docs/reference/transport-contracts.mdx +17 -0
- package/site/content/docs/reference/webhook-events.mdx +70 -0
- package/site/content/docs/reference/whatsapp-options.mdx +15 -0
- package/site/content/docs/transports/brevo.mdx +40 -0
- package/site/content/docs/transports/cloudflare-email.mdx +40 -0
- package/site/content/docs/transports/index.mdx +105 -0
- package/site/content/docs/transports/loops.mdx +41 -0
- package/site/content/docs/transports/mailersend.mdx +40 -0
- package/site/content/docs/transports/mailgun.mdx +42 -0
- package/site/content/docs/transports/mailtrap.mdx +42 -0
- package/site/content/docs/transports/meta.json +32 -0
- package/site/content/docs/transports/msegat.mdx +42 -0
- package/site/content/docs/transports/plunk.mdx +40 -0
- package/site/content/docs/transports/postmark.mdx +40 -0
- package/site/content/docs/transports/resend.mdx +43 -0
- package/site/content/docs/transports/sendgrid.mdx +40 -0
- package/site/content/docs/transports/ses.mdx +44 -0
- package/site/content/docs/transports/smtp.mdx +47 -0
- package/site/content/docs/transports/sndr.mdx +144 -0
- package/site/content/docs/transports/sparkpost.mdx +41 -0
- package/site/content/docs/transports/taqnyat-mail.mdx +41 -0
- package/site/content/docs/transports/taqnyat-sms.mdx +41 -0
- package/site/content/docs/transports/taqnyat-whatsapp.mdx +40 -0
- package/site/content/docs/transports/twilio-sms.mdx +128 -0
- package/site/content/docs/transports/webpush.mdx +43 -0
- package/site/content/docs/transports/whatsapp-cloud.mdx +42 -0
- package/dist/chunk-5scdgffb.js +0 -5
- package/dist/chunk-8phpsns1.js +0 -13
- package/dist/chunk-m07smapm.js +0 -3
- package/dist/chunk-n5dan5bz.js +0 -4
- package/dist/chunk-qh90yr6d.js +0 -4
- package/dist/chunk-vhxzhx8b.js +0 -4
- package/dist/chunk-wbvhaqcp.js +0 -4
- package/dist/chunk-wsymfbkn.js +0 -4
- package/dist/chunk-yr56b0ts.js +0 -4
- package/dist/chunk-yr56b0ts.js.map +0 -11
package/README.md
CHANGED
|
@@ -1,991 +1,120 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source
|
|
4
|
+
media="(prefers-color-scheme: dark)"
|
|
5
|
+
srcset="https://shieldcn.dev/header/glow.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/main/site/public/sentlyIconLogo-w.svg&theme=zinc&size=banner&mode=dark&font=geist"
|
|
6
|
+
/>
|
|
7
|
+
<img
|
|
8
|
+
alt="sently — One API. Four channels. Every runtime."
|
|
9
|
+
src="https://shieldcn.dev/header/glow.svg?title=sently&subtitle=One+API.+Four+channels.+Every+runtime.&logo=https://raw.githubusercontent.com/alialnaghmoush/sently/main/site/public/sentlyIconLogo-k.svg&theme=zinc&size=banner&mode=light&font=geist"
|
|
10
|
+
width="750"
|
|
11
|
+
/>
|
|
12
|
+
</picture>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.npmjs.com/package/sently"><img alt="npm" src="https://shieldcn.dev/npm/sently.svg?size=sm" /></a>
|
|
17
|
+
<a href="https://jsr.io/@alialnaghmoush/sently"><img alt="JSR" src="https://shieldcn.dev/jsr/@alialnaghmoush/sently.svg?size=sm&variant=outline" /></a>
|
|
18
|
+
<a href="https://bundlephobia.com/package/sently"><img alt="bundle" src="https://shieldcn.dev/bundlephobia/minzip/sently.svg?size=sm&variant=secondary" /></a>
|
|
19
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="MIT" src="https://shieldcn.dev/npm/license/sently.svg?size=sm" /></a>
|
|
20
|
+
<a href="https://bun.sh"><img alt="Bun" src="https://shieldcn.dev/badge/Bun-ready-000000.svg?logo=bun&size=sm&variant=outline" /></a>
|
|
21
|
+
<a href="https://github.com/alialnaghmoush/sently/stargazers"><img alt="stars" src="https://shieldcn.dev/github/stars/alialnaghmoush/sently.svg?size=sm&variant=outline" /></a>
|
|
22
|
+
<a href="https://github.com/alialnaghmoush/sently/actions"><img alt="CI" src="https://shieldcn.dev/github/ci/alialnaghmoush/sently.svg?size=sm" /></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<em>Stop wiring vendor SDKs into every channel. One sender shape for email, SMS, WhatsApp, and push — swap the transport, keep your call sites. Node, Bun, Deno, Workers.</em>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<a href="https://sently.omqkhafi.dev"><strong>Docs</strong></a> ·
|
|
31
|
+
<a href="https://sently.omqkhafi.dev/docs"><strong>Handbook</strong></a> ·
|
|
32
|
+
<a href="https://sently.omqkhafi.dev/llms.txt"><code>llms.txt</code></a> ·
|
|
33
|
+
<a href="https://www.npmjs.com/package/sently"><code>sently</code></a>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
> [!WARNING]
|
|
37
|
+
> **Early development (`v0.x`) — API may change.**
|
|
38
|
+
>
|
|
39
|
+
> Pin an exact version for production until v1.0.0. See [CHANGELOG](CHANGELOG.md).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
5
42
|
|
|
6
43
|
```bash
|
|
7
|
-
bun add sently
|
|
44
|
+
bun add sently # npm / Bun / yarn / pnpm
|
|
45
|
+
bunx jsr add @alialnaghmoush/sently # JSR
|
|
8
46
|
```
|
|
9
47
|
|
|
10
|
-
|
|
11
|
-
[](https://jsr.io/@alialnaghmoush/sently)
|
|
12
|
-
[](https://bundlephobia.com/package/sently)
|
|
13
|
-
[](LICENSE)
|
|
14
|
-
[](#)
|
|
15
|
-
[](https://github.com/alialnaghmoush/sently)
|
|
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.8.0"`) for production until v1.0.0.
|
|
18
|
-
|
|
19
|
-
## Index
|
|
20
|
-
|
|
21
|
-
**Getting started**
|
|
22
|
-
|
|
23
|
-
- [Why not Nodemailer?](#why-not-nodemailer)
|
|
24
|
-
- [The 30-second tour](#the-30-second-tour)
|
|
25
|
-
- [Installation](#installation)
|
|
26
|
-
- [Quick Start](#quick-start)
|
|
27
|
-
- [SMTP with auto-detected adapter](#smtp-with-auto-detected-adapter)
|
|
28
|
-
- [Resend HTTP transport](#resend-http-transport-vercel-edge-compatible)
|
|
29
|
-
- [Cloudflare Worker](#cloudflare-worker)
|
|
30
|
-
- [Choosing an entrypoint](#choosing-an-entrypoint)
|
|
31
|
-
- [Migrating from Nodemailer](#migrating-from-nodemailer)
|
|
32
|
-
|
|
33
|
-
**Sending mail**
|
|
34
|
-
|
|
35
|
-
- [Adapters](#adapters)
|
|
36
|
-
- [Transports](#transports)
|
|
37
|
-
- [SMTP](#smtp)
|
|
38
|
-
- [HTTP APIs](#http-apis)
|
|
39
|
-
- [FallbackTransport](#fallbacktransport)
|
|
40
|
-
- [PreviewTransport](#previewtransport)
|
|
41
|
-
- [RetryTransport](#retrytransport)
|
|
42
|
-
- [sendBulk()](#sendbulk)
|
|
43
|
-
- [Mailer lifecycle hooks](#mailer-lifecycle-hooks)
|
|
44
|
-
- [IdempotencyTransport](#idempotencytransport)
|
|
45
|
-
- [Plugin system](#plugin-system)
|
|
46
|
-
- [TemplatePlugin](#templateplugin)
|
|
47
|
-
- [React Email plugin](#react-email-plugin)
|
|
48
|
-
- [Webhook parsing](#webhook-parsing)
|
|
49
|
-
|
|
50
|
-
**Reference**
|
|
51
|
-
|
|
52
|
-
- [MailOptions Reference](#mailoptions-reference)
|
|
53
|
-
- [Attachments](#attachments)
|
|
54
|
-
- [Error Handling](#error-handling)
|
|
55
|
-
- [Security](#security)
|
|
56
|
-
- [Bundle size](#bundle-size)
|
|
57
|
-
- [TypeScript](#typescript)
|
|
58
|
-
- [Links](#links)
|
|
59
|
-
- [License](#license)
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Why not Nodemailer?
|
|
48
|
+
Optional peers (React Email only): `react`, `@react-email/render`.
|
|
64
49
|
|
|
65
|
-
|
|
66
|
-
|---------|-----------|--------|
|
|
67
|
-
| Bundle size | ~58 KB gzip always ([v8.0.10](https://bundlephobia.com/package/nodemailer@8.0.10)) | ~6.1 KB HTTP · ~15 KB SMTP |
|
|
68
|
-
| Runtimes | Node.js only | Node, Bun, Deno, CF Workers |
|
|
69
|
-
| Module format | CommonJS | ESM only |
|
|
70
|
-
| Dependencies | 0 | 0 |
|
|
71
|
-
| DKIM signing | ✓ via `nodemailer-dkim` | ✓ built-in (Web Crypto) |
|
|
72
|
-
| OAuth2 / XOAUTH2 | ✓ via plugin | ✓ built-in |
|
|
73
|
-
| Connection pooling | ✓ | ✓ |
|
|
74
|
-
| HTTP transports | ✓ via plugins | ✓ built-in (11 HTTP APIs + CF Email binding) |
|
|
75
|
-
| Provider failover | ✗ | ✓ `FallbackTransport` + weighted routing |
|
|
76
|
-
| Retry transport | ✗ | ✓ |
|
|
77
|
-
| Preview transport | ✗ | ✓ |
|
|
78
|
-
| Template engine | ✗ | ✓ |
|
|
79
|
-
| `sendBulk()` | ✗ | ✓ (native batch on Resend/SendGrid) |
|
|
80
|
-
| React Email | ✗ via plugin | ✓ `sently/react` |
|
|
81
|
-
| Idempotency keys | ✗ | ✓ `sently/idempotency` |
|
|
82
|
-
| Webhook parsing | ✗ | ✓ `sently/webhooks` |
|
|
83
|
-
| TypeScript | via `@types/nodemailer` | ✓ built-in |
|
|
84
|
-
| Last release | 2026 (8.0.x) | 2026 |
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## The 30-second tour
|
|
50
|
+
## Quick start
|
|
89
51
|
|
|
90
52
|
```typescript
|
|
91
|
-
import type { MailOptions } from "sently";
|
|
92
53
|
import { createMailer } from "sently/mailer";
|
|
93
54
|
import { ResendTransport } from "sently/transports/resend";
|
|
94
|
-
import { PreviewTransport } from "sently/transports/preview";
|
|
95
|
-
|
|
96
|
-
const addFooter = (options: MailOptions): MailOptions => ({
|
|
97
|
-
...options,
|
|
98
|
-
html: (options.html ?? "") + '<p style="color:#999">Unsubscribe</p>',
|
|
99
|
-
});
|
|
100
55
|
|
|
101
|
-
// Swap providers without changing send code
|
|
102
56
|
const mailer = await createMailer({
|
|
103
57
|
transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
|
|
104
|
-
plugins: [addFooter],
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
await mailer.send({
|
|
108
|
-
from: "you@example.com",
|
|
109
|
-
to: "recipient@example.com",
|
|
110
|
-
subject: "Hello from sently",
|
|
111
|
-
html: "<p>Hello!</p>",
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Bulk send with concurrency control
|
|
115
|
-
await mailer.sendBulk(recipients, { concurrency: 5 });
|
|
116
|
-
|
|
117
|
-
// Local dev — write to disk instead of sending
|
|
118
|
-
const devMailer = await createMailer({
|
|
119
|
-
transport: process.env.CI
|
|
120
|
-
? new ResendTransport({ apiKey: process.env.RESEND_API_KEY! })
|
|
121
|
-
: new PreviewTransport({ outDir: ".emails", open: true }),
|
|
122
|
-
});
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## Installation
|
|
128
|
-
|
|
129
|
-
**npm** ([sently](https://www.npmjs.com/package/sently)):
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
bun add sently
|
|
133
|
-
npm install sently
|
|
134
|
-
pnpm add sently
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
**JSR** ([@alialnaghmoush/sently](https://jsr.io/@alialnaghmoush/sently)) — Deno, Bun, and other JSR-aware runtimes:
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
deno add jsr:@alialnaghmoush/sently
|
|
141
|
-
bunx jsr add @alialnaghmoush/sently
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
import { createMailer } from "sently/mailer"; // HTTP stack ~6.1 KB with a transport
|
|
146
|
-
import { createSMTPMailer } from "sently/smtp"; // SMTP relay ~15 KB
|
|
147
|
-
// Or: import { createSMTPMailer } from "sently";
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
## Quick Start
|
|
153
|
-
|
|
154
|
-
### SMTP with auto-detected adapter
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
158
|
-
|
|
159
|
-
const mailer = await createSMTPMailer({
|
|
160
|
-
host: "smtp.example.com",
|
|
161
|
-
port: 587,
|
|
162
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
163
58
|
});
|
|
164
59
|
|
|
165
60
|
await mailer.send({
|
|
166
|
-
from: "
|
|
167
|
-
to: "recipient@example.com",
|
|
168
|
-
subject: "Hello from sently",
|
|
169
|
-
text: "Plain text body",
|
|
170
|
-
html: "<p>HTML body</p>",
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
await mailer.close();
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Resend HTTP transport (Vercel Edge compatible)
|
|
177
|
-
|
|
178
|
-
```typescript
|
|
179
|
-
import { createMailer } from "sently/mailer";
|
|
180
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
181
|
-
|
|
182
|
-
const mailer = await createMailer({
|
|
183
|
-
transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
await mailer.send({
|
|
187
|
-
from: "onboarding@yourdomain.com",
|
|
188
|
-
to: "recipient@example.com",
|
|
189
|
-
subject: "Hello from the edge",
|
|
190
|
-
html: "<p>Sent via Resend + sently</p>",
|
|
191
|
-
});
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Cloudflare Worker
|
|
195
|
-
|
|
196
|
-
**SMTP relay** (outbound TCP via `cloudflare:sockets`):
|
|
197
|
-
|
|
198
|
-
```typescript
|
|
199
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
200
|
-
import { CloudflareAdapter } from "sently/adapters/cf";
|
|
201
|
-
|
|
202
|
-
export default {
|
|
203
|
-
async fetch() {
|
|
204
|
-
const mailer = await createSMTPMailer({
|
|
205
|
-
host: "smtp.example.com",
|
|
206
|
-
port: 587,
|
|
207
|
-
auth: { user: "relay@example.com", pass: "secret" },
|
|
208
|
-
adapter: new CloudflareAdapter(),
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
await mailer.send({
|
|
212
|
-
from: "relay@example.com",
|
|
213
|
-
to: "user@example.com",
|
|
214
|
-
subject: "From a Worker",
|
|
215
|
-
text: "Hello from Cloudflare Workers",
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
return new Response("Sent");
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
**Workers Email binding** (`[[send_email]]` in `wrangler.toml` — no fetch HTTP API):
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
import { createMailer } from "sently/mailer";
|
|
227
|
-
import { CloudflareEmailTransport } from "sently/transports/cloudflare-email";
|
|
228
|
-
|
|
229
|
-
export default {
|
|
230
|
-
async fetch(_request, env) {
|
|
231
|
-
const mailer = await createMailer({
|
|
232
|
-
transport: new CloudflareEmailTransport({ sendEmail: env.SEND_EMAIL }),
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
await mailer.send({
|
|
236
|
-
from: "noreply@yourdomain.com",
|
|
237
|
-
to: "user@example.com",
|
|
238
|
-
subject: "From a Worker",
|
|
239
|
-
text: "Sent via send_email binding",
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
return new Response("Sent");
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
|
-
## Adapters
|
|
250
|
-
|
|
251
|
-
| Runtime | Import | Notes |
|
|
252
|
-
|---------|--------|-------|
|
|
253
|
-
| Node.js (auto) | `createSMTPMailer` from `sently/smtp` | Auto-detected adapter |
|
|
254
|
-
| Node.js (explicit) | `sently/adapters/node` → `NodeAdapter` | Reference implementation |
|
|
255
|
-
| Bun (auto) | `createSMTPMailer` from `sently/smtp` | Auto-detected adapter |
|
|
256
|
-
| Bun (explicit) | `sently/adapters/bun` → `BunAdapter` | Node compat layer |
|
|
257
|
-
| Deno | `sently/adapters/deno` → `DenoAdapter` | Native `Deno.startTls` |
|
|
258
|
-
| Cloudflare Workers | `sently/adapters/cf` → `CloudflareAdapter` | `cloudflare:sockets` |
|
|
259
|
-
|
|
260
|
-
```typescript
|
|
261
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
262
|
-
import { NodeAdapter } from "sently/adapters/node";
|
|
263
|
-
|
|
264
|
-
const mailer = await createSMTPMailer({
|
|
265
|
-
host: "smtp.example.com",
|
|
266
|
-
adapter: new NodeAdapter({ secure: false }),
|
|
267
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
268
|
-
});
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
## Transports
|
|
274
|
-
|
|
275
|
-
### SMTP
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
import { createMailer } from "sently/mailer";
|
|
279
|
-
import { SMTPTransport } from "sently/transports/smtp";
|
|
280
|
-
import { NodeAdapter } from "sently/adapters/node";
|
|
281
|
-
|
|
282
|
-
const transport = new SMTPTransport({
|
|
283
|
-
host: "smtp.example.com",
|
|
284
|
-
port: 587,
|
|
285
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
286
|
-
adapter: new NodeAdapter(),
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const mailer = await createMailer({ transport });
|
|
290
|
-
await mailer.verify(); // test connection + auth
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
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.
|
|
294
|
-
|
|
295
|
-
**AUTH methods:** XOAUTH2, CRAM-MD5, LOGIN, and PLAIN (auto-negotiated from EHLO unless `auth.type` is set).
|
|
296
|
-
|
|
297
|
-
**`requireTLS` (default `true` when `auth` is set):** sently refuses to send credentials over an unencrypted connection. If the link is not secured by direct TLS (`secure: true`) or a successful `STARTTLS` upgrade, authentication throws an `SMTPError` instead of leaking credentials — this defends against STARTTLS-stripping MITM attacks. Set `requireTLS: false` only if you fully trust the network (not recommended).
|
|
298
|
-
|
|
299
|
-
#### DKIM signing
|
|
300
|
-
|
|
301
|
-
```typescript
|
|
302
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
303
|
-
|
|
304
|
-
const mailer = await createSMTPMailer({
|
|
305
|
-
host: "smtp.example.com",
|
|
306
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
307
|
-
dkim: {
|
|
308
|
-
domainName: "example.com",
|
|
309
|
-
keySelector: "2024",
|
|
310
|
-
privateKey: await Bun.file("dkim-private.pem").text(),
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Pass `dkim` on SMTP config or use `signDKIM` from `sently/dkim` directly. MIME lazy-loads DKIM only when the option is set.
|
|
316
|
-
|
|
317
|
-
#### Gmail OAuth2 (XOAUTH2)
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
321
|
-
|
|
322
|
-
const mailer = await createSMTPMailer({
|
|
323
|
-
host: "smtp.gmail.com",
|
|
324
|
-
port: 465,
|
|
325
|
-
secure: true,
|
|
326
|
-
auth: {
|
|
327
|
-
type: "OAUTH2",
|
|
328
|
-
user: "me@gmail.com",
|
|
329
|
-
oauth2: {
|
|
330
|
-
user: "me@gmail.com",
|
|
331
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
332
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
333
|
-
refreshToken: process.env.GOOGLE_REFRESH_TOKEN!,
|
|
334
|
-
},
|
|
335
|
-
},
|
|
336
|
-
});
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
#### Microsoft 365 OAuth2 (XOAUTH2)
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
import { MICROSOFT_TOKEN_URL } from "sently";
|
|
343
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
344
|
-
|
|
345
|
-
const mailer = await createSMTPMailer({
|
|
346
|
-
host: "smtp.office365.com",
|
|
347
|
-
port: 587,
|
|
348
|
-
auth: {
|
|
349
|
-
type: "OAUTH2",
|
|
350
|
-
user: "you@yourtenant.onmicrosoft.com",
|
|
351
|
-
oauth2: {
|
|
352
|
-
user: "you@yourtenant.onmicrosoft.com",
|
|
353
|
-
clientId: process.env.AZURE_CLIENT_ID!,
|
|
354
|
-
clientSecret: process.env.AZURE_CLIENT_SECRET!,
|
|
355
|
-
refreshToken: process.env.AZURE_REFRESH_TOKEN!,
|
|
356
|
-
tokenUrl: MICROSOFT_TOKEN_URL,
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
});
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
#### Connection pooling
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
366
|
-
|
|
367
|
-
const mailer = await createSMTPMailer({
|
|
368
|
-
host: "smtp.example.com",
|
|
369
|
-
pool: true,
|
|
370
|
-
maxConnections: 5,
|
|
371
|
-
maxMessages: 100,
|
|
372
|
-
rateDelta: 10,
|
|
373
|
-
rateLimit: 1000,
|
|
374
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
375
|
-
});
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
Or use `SMTPPool` directly:
|
|
379
|
-
|
|
380
|
-
```typescript
|
|
381
|
-
import { SMTPPool } from "sently/pool";
|
|
382
|
-
|
|
383
|
-
const pool = new SMTPPool({
|
|
384
|
-
host: "smtp.example.com",
|
|
385
|
-
adapter: new NodeAdapter(),
|
|
386
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
387
|
-
});
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### HTTP APIs
|
|
391
|
-
|
|
392
|
-
| Transport | Import path | Required config |
|
|
393
|
-
|-----------|-------------|-----------------|
|
|
394
|
-
| Mailer wrapper | `sently/mailer` | — (use with any transport below) |
|
|
395
|
-
| Resend | `sently/transports/resend` | `apiKey` |
|
|
396
|
-
| SendGrid | `sently/transports/sendgrid` | `apiKey` |
|
|
397
|
-
| Postmark | `sently/transports/postmark` | `serverToken` |
|
|
398
|
-
| Mailgun | `sently/transports/mailgun` | `apiKey`, `domain` |
|
|
399
|
-
| AWS SES | `sently/transports/ses` | `accessKeyId`, `secretAccessKey`, `region` |
|
|
400
|
-
| Brevo | `sently/transports/brevo` | `apiKey` |
|
|
401
|
-
| MailerSend | `sently/transports/mailersend` | `apiToken` |
|
|
402
|
-
| Plunk | `sently/transports/plunk` | `apiKey` |
|
|
403
|
-
| SparkPost | `sently/transports/sparkpost` | `apiKey`, `euRegion?` |
|
|
404
|
-
| Mailtrap | `sently/transports/mailtrap` | `apiToken`, `sandbox?`, `inboxId?` |
|
|
405
|
-
| Loops | `sently/transports/loops` | `apiKey`, `defaultTransactionalId?` |
|
|
406
|
-
| Cloudflare Email | `sently/transports/cloudflare-email` | `sendEmail` binding (`env.SEND_EMAIL`) |
|
|
407
|
-
|
|
408
|
-
All transports implement the same interface — swap without changing your send code.
|
|
409
|
-
|
|
410
|
-
**Routing decorators** (compose with any transport above):
|
|
411
|
-
|
|
412
|
-
| Transport | Import path | Purpose |
|
|
413
|
-
|-----------|-------------|---------|
|
|
414
|
-
| Fallback | `sently/transports/fallback` | Ordered provider failover |
|
|
415
|
-
| Weighted fallback | `sently/transports/weighted-fallback` | Weighted-random primary + failover |
|
|
416
|
-
| Retry | `sently/transports/retry` | Per-provider retries before failing over |
|
|
417
|
-
| Idempotency | `sently/idempotency` | Dedupe sends on retry/replay |
|
|
418
|
-
|
|
419
|
-
**Loops** is template-first: `subject`/`html`/`text` are ignored. Set `options.headers['x-loops-transactional-id']` (or `defaultTransactionalId` on the transport) and pass template variables via `options.data`.
|
|
420
|
-
|
|
421
|
-
**Plunk** sends one HTTP request per `to` address and aggregates results when multiple recipients are provided.
|
|
422
|
-
|
|
423
|
-
### FallbackTransport
|
|
424
|
-
|
|
425
|
-
Route through an ordered list of providers — if your primary has an outage, the next takes over. Compose with `RetryTransport` to retry within a provider before failing over:
|
|
426
|
-
|
|
427
|
-
```typescript
|
|
428
|
-
import { FallbackTransport } from "sently/transports/fallback";
|
|
429
|
-
import { RetryTransport } from "sently/transports/retry";
|
|
430
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
431
|
-
import { SESTransport } from "sently/transports/ses";
|
|
432
|
-
import { createMailer } from "sently/mailer";
|
|
433
|
-
|
|
434
|
-
const transport = new FallbackTransport([
|
|
435
|
-
new RetryTransport(new ResendTransport({ apiKey: process.env.RESEND_API_KEY! })),
|
|
436
|
-
new RetryTransport(
|
|
437
|
-
new SESTransport({
|
|
438
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
439
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
440
|
-
}),
|
|
441
|
-
),
|
|
442
|
-
]);
|
|
443
|
-
|
|
444
|
-
const mailer = await createMailer({ transport });
|
|
445
|
-
|
|
446
|
-
const result = await mailer.send({ from: "...", to: "...", subject: "...", html: "..." });
|
|
447
|
-
// result.provider === "ses", result.providerIndex === 1 → primary failed, secondary won
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
Permanent client errors (HTTP 400/401/403, SMTP 535) are **not** retried on the next provider — they would fail identically everywhere. When all providers fail, `FallbackError.attempts` lists each `{ provider, error }` in order for debugging.
|
|
451
|
-
|
|
452
|
-
**Cooldown** — skip providers that recently failed until a cooldown expires:
|
|
453
|
-
|
|
454
|
-
```typescript
|
|
455
|
-
const transport = new FallbackTransport(transports, { cooldownMs: 300_000 });
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
**Full-chain verify** — `verify()` returns the first healthy provider; use `verifyAll()` for per-provider visibility:
|
|
459
|
-
|
|
460
|
-
```typescript
|
|
461
|
-
const { ok, providers } = await transport.verifyAll();
|
|
462
|
-
// providers: [{ provider: "resend", ok: true }, { provider: "ses", ok: false, message: "..." }]
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
**Mailer `onFallback` hook** — observability when failover happens (requires `FallbackTransport` in the stack):
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
const mailer = await createMailer({
|
|
469
|
-
transport,
|
|
470
|
-
hooks: {
|
|
471
|
-
onFallback: (_ctx, failedProvider, nextProvider, error) => {
|
|
472
|
-
console.log(`failover ${failedProvider} → ${nextProvider}`, error);
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
|
-
});
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
**Weighted routing** — shift traffic gradually between providers:
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
import { WeightedFallbackTransport } from "sently/transports/weighted-fallback";
|
|
482
|
-
|
|
483
|
-
const transport = new WeightedFallbackTransport([
|
|
484
|
-
{ transport: new ResendTransport({ apiKey }), weight: 80 },
|
|
485
|
-
{ transport: new SESTransport({ accessKeyId, secretAccessKey }), weight: 20 },
|
|
486
|
-
]);
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
Every transport exposes a stable **`Transport.provider`** string (e.g. `"resend"`, `"ses"`) for hooks, logs, and `SendResult.provider`.
|
|
490
|
-
|
|
491
|
-
**Cloudflare Workers Email** — use the `send_email` binding (not fetch HTTP). Configure `[[send_email]]` in `wrangler.toml`, then pass `env.SEND_EMAIL`:
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
import { CloudflareEmailTransport } from "sently/transports/cloudflare-email";
|
|
495
|
-
|
|
496
|
-
const mailer = await createMailer({
|
|
497
|
-
transport: new CloudflareEmailTransport({ sendEmail: env.SEND_EMAIL }),
|
|
498
|
-
});
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
Attachments are base64-encoded in the binding payload; use `content: Uint8Array` on Workers (no `attachment.path`).
|
|
502
|
-
|
|
503
|
-
### PreviewTransport
|
|
504
|
-
|
|
505
|
-
Write emails to disk during local development instead of sending them:
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
import { PreviewTransport } from "sently/transports/preview";
|
|
509
|
-
import { createMailer } from "sently/mailer";
|
|
510
|
-
|
|
511
|
-
const mailer = await createMailer({
|
|
512
|
-
transport: new PreviewTransport({
|
|
513
|
-
outDir: "./.emails",
|
|
514
|
-
open: true,
|
|
515
|
-
format: "html",
|
|
516
|
-
}),
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
await mailer.send({
|
|
520
|
-
from: "dev@localhost",
|
|
61
|
+
from: "hello@example.com",
|
|
521
62
|
to: "you@example.com",
|
|
522
|
-
subject: "Preview me",
|
|
523
|
-
html: "<h1>Hello</h1>",
|
|
524
|
-
});
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
### RetryTransport
|
|
528
|
-
|
|
529
|
-
Wrap any transport with automatic retries and configurable backoff:
|
|
530
|
-
|
|
531
|
-
```typescript
|
|
532
|
-
import { RetryTransport } from "sently/transports/retry";
|
|
533
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
534
|
-
import { createMailer } from "sently/mailer";
|
|
535
|
-
|
|
536
|
-
const transport = new RetryTransport(
|
|
537
|
-
new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
|
|
538
|
-
{ maxAttempts: 3, backoff: "exponential", retryOn: [429, 503] },
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
const mailer = await createMailer({ transport });
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
### sendBulk()
|
|
545
|
-
|
|
546
|
-
Send multiple messages with concurrency control and per-message callbacks. When the transport implements `sendBatch` (Resend, SendGrid), attachment-free messages are sent via native batch endpoints; messages with attachments fall back to individual sends.
|
|
547
|
-
|
|
548
|
-
```typescript
|
|
549
|
-
const result = await mailer.sendBulk(
|
|
550
|
-
[
|
|
551
|
-
{ from: "a@b.com", to: "1@example.com", subject: "One", text: "Hi" },
|
|
552
|
-
{ from: "a@b.com", to: "2@example.com", subject: "Two", text: "Hi" },
|
|
553
|
-
],
|
|
554
|
-
{
|
|
555
|
-
concurrency: 2,
|
|
556
|
-
stopOnError: false, // halt remaining sends after first failure when true
|
|
557
|
-
onSuccess: (_msg, index) => console.log(`Sent #${index}`),
|
|
558
|
-
onError: (_msg, index, err) => console.error(`Failed #${index}`, err),
|
|
559
|
-
},
|
|
560
|
-
);
|
|
561
|
-
|
|
562
|
-
console.log(result.sent, result.failed);
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
Resend batches up to `RESEND_BATCH_MAX` (100) messages per request — export from `sently/transports/resend`.
|
|
566
|
-
|
|
567
|
-
### Mailer lifecycle hooks
|
|
568
|
-
|
|
569
|
-
Optional observability hooks on `createMailer` fire for every `send()` and `sendBulk()` message (batch paths invoke hooks once per message, without double-firing). Hook context carries `{ messageId?, to, subject, provider }` — no body fields, to avoid leaking PII into logs. `onSuccess` and `onError` accept an optional third argument `durationMs` (elapsed milliseconds).
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
import { consoleObserver } from "sently/observability";
|
|
573
|
-
|
|
574
|
-
const mailer = await createMailer({
|
|
575
|
-
transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
|
|
576
|
-
hooks: {
|
|
577
|
-
onSend: (ctx) => metrics.increment("email.send", { provider: ctx.provider }),
|
|
578
|
-
onSuccess: (ctx, result, durationMs) =>
|
|
579
|
-
metrics.histogram("email.duration", durationMs ?? 0),
|
|
580
|
-
onError: (ctx, err, durationMs) => metrics.increment("email.error"),
|
|
581
|
-
onRetry: (ctx, attempt, err) => metrics.increment("email.retry", { attempt }),
|
|
582
|
-
onFallback: (ctx, failed, next, err) =>
|
|
583
|
-
metrics.increment("email.fallback", { from: failed, to: next }),
|
|
584
|
-
},
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// Quick start — log lifecycle events to the console
|
|
588
|
-
const devMailer = await createMailer({
|
|
589
|
-
transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
|
|
590
|
-
hooks: consoleObserver("[myapp]"),
|
|
591
|
-
});
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
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; pair `onFallback` with `FallbackTransport` or `WeightedFallbackTransport` for failover observability.
|
|
595
|
-
|
|
596
|
-
Works with both `sently/mailer` (`{ transport, hooks }`) and SMTP config via `createSMTPMailer` (`{ host, auth, hooks }`).
|
|
597
|
-
|
|
598
|
-
### IdempotencyTransport
|
|
599
|
-
|
|
600
|
-
Prevent duplicate sends on retry or replay. Wrap **outside** `RetryTransport` so all retry attempts share one key:
|
|
601
|
-
|
|
602
|
-
```typescript
|
|
603
|
-
import { IdempotencyTransport } from "sently/idempotency";
|
|
604
|
-
import { RetryTransport } from "sently/transports/retry";
|
|
605
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
606
|
-
|
|
607
|
-
const transport = new IdempotencyTransport(
|
|
608
|
-
new RetryTransport(new ResendTransport({ apiKey: process.env.RESEND_API_KEY! })),
|
|
609
|
-
{ ttlMs: 86_400_000 },
|
|
610
|
-
);
|
|
611
|
-
|
|
612
|
-
await mailer.send({
|
|
613
|
-
from: "you@example.com",
|
|
614
|
-
to: "user@example.com",
|
|
615
|
-
subject: "Hello",
|
|
616
|
-
text: "Hi",
|
|
617
|
-
idempotencyKey: "order-123-email", // or derive from messageId
|
|
618
|
-
});
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
Resend sends the `Idempotency-Key` HTTP header natively. Supply a shared store (Redis, Dragonfly) in production — `MemoryIdempotencyStore` is for single-process use.
|
|
622
|
-
|
|
623
|
-
---
|
|
624
|
-
|
|
625
|
-
## Plugin system
|
|
626
|
-
|
|
627
|
-
Plugins transform `MailOptions` before the transport builds and sends the message. They run sequentially — each receives the output of the previous plugin.
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
import type { MailOptions } from "sently";
|
|
631
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
632
|
-
|
|
633
|
-
const addFooter = (options: MailOptions) => ({
|
|
634
|
-
...options,
|
|
635
|
-
html: (options.html ?? "") + '<p style="color:#999">Unsubscribe</p>',
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
const mailer = await createSMTPMailer({
|
|
639
|
-
host: "smtp.resend.com",
|
|
640
|
-
port: 465,
|
|
641
|
-
secure: true,
|
|
642
|
-
auth: { user: "resend", pass: process.env.RESEND_API_KEY! },
|
|
643
|
-
plugins: [addFooter],
|
|
644
|
-
});
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
Works with SMTP config or custom transports:
|
|
648
|
-
|
|
649
|
-
```typescript
|
|
650
|
-
import { createMailer } from "sently/mailer";
|
|
651
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
652
|
-
|
|
653
|
-
const mailer = await createMailer({
|
|
654
|
-
transport: new ResendTransport({ apiKey: "re_..." }),
|
|
655
|
-
plugins: [addFooter],
|
|
656
|
-
});
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### TemplatePlugin
|
|
660
|
-
|
|
661
|
-
Render HTML from named templates with zero dependencies:
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
import { templatePlugin, simpleEngine } from "sently/plugins/template";
|
|
665
|
-
import { createMailer } from "sently/mailer";
|
|
666
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
667
|
-
|
|
668
|
-
const mailer = await createMailer({
|
|
669
|
-
transport: new ResendTransport({ apiKey: "re_..." }),
|
|
670
|
-
plugins: [
|
|
671
|
-
templatePlugin({
|
|
672
|
-
engine: simpleEngine,
|
|
673
|
-
templates: {
|
|
674
|
-
welcome: "<h1>Hello, {{name}}!</h1>",
|
|
675
|
-
},
|
|
676
|
-
}),
|
|
677
|
-
],
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
await mailer.send({
|
|
681
|
-
from: "onboarding@yourdomain.com",
|
|
682
|
-
to: "user@example.com",
|
|
683
63
|
subject: "Welcome",
|
|
684
|
-
|
|
685
|
-
data: { name: "Ali" },
|
|
64
|
+
html: "<p>Sent with sently.</p>",
|
|
686
65
|
});
|
|
687
66
|
```
|
|
688
67
|
|
|
689
|
-
|
|
68
|
+
Same shape for every channel — apps use **sently senders**, not vendor SDKs:
|
|
690
69
|
|
|
691
|
-
|
|
70
|
+
| Channel | Sender | Example transport |
|
|
71
|
+
| -------- | ------------------------------------ | ------------------------------------------ |
|
|
72
|
+
| Email | `createMailer` / `createSMTPMailer` | `sently/transports/resend`, `sently/smtp` |
|
|
73
|
+
| SMS | `createSmsSender` | `sently/transports/twilio-sms` |
|
|
74
|
+
| WhatsApp | `createWhatsAppSender` | `sently/transports/whatsapp-cloud` |
|
|
75
|
+
| Push | `createPushSender` | `sently/transports/webpush` |
|
|
692
76
|
|
|
693
|
-
|
|
77
|
+
Full walkthrough: [Get started](https://sently.omqkhafi.dev/docs/get-started).
|
|
694
78
|
|
|
695
|
-
|
|
696
|
-
import { reactPlugin } from "sently/react";
|
|
697
|
-
import { createMailer } from "sently/mailer";
|
|
698
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
699
|
-
import { WelcomeEmail } from "./emails/welcome";
|
|
79
|
+
## Why sently?
|
|
700
80
|
|
|
701
|
-
|
|
702
|
-
transport: new ResendTransport({ apiKey: "re_..." }),
|
|
703
|
-
plugins: [reactPlugin()],
|
|
704
|
-
});
|
|
81
|
+
Nodemailer is Node.js–only and ships the full mail stack on every import (~59 KB gzip for [v9.0.3](https://bundlephobia.com/package/nodemailer@9.0.3)). sently is tree-shakeable, multi-runtime, and multi-channel.
|
|
705
82
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
### Webhook parsing
|
|
717
|
-
|
|
718
|
-
Normalize provider webhooks into a single event type — no server framework required:
|
|
719
|
-
|
|
720
|
-
```typescript
|
|
721
|
-
import { parseResendWebhook, parseSesWebhook } from "sently/webhooks";
|
|
722
|
-
|
|
723
|
-
// Resend (Svix-style payload)
|
|
724
|
-
const events = parseResendWebhook(await request.json());
|
|
725
|
-
|
|
726
|
-
// AWS SES via SNS (handles SubscriptionConfirmation + double-encoded Message)
|
|
727
|
-
const sesEvents = parseSesWebhook(await request.json());
|
|
728
|
-
|
|
729
|
-
for (const event of events) {
|
|
730
|
-
console.log(event.type, event.messageId, event.recipient);
|
|
731
|
-
}
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
Parsers: Resend, SendGrid, Postmark, Mailgun, SES, Brevo. Optional HMAC verification helpers for Mailgun and Resend (`verifyMailgunSignature`, `verifyResendSignature`).
|
|
735
|
-
|
|
736
|
-
---
|
|
737
|
-
|
|
738
|
-
## MailOptions Reference
|
|
739
|
-
|
|
740
|
-
| Field | Type | Default | Description |
|
|
741
|
-
|-------|------|---------|-------------|
|
|
742
|
-
| `from` | `AddressInput` | *required* | Sender address |
|
|
743
|
-
| `to` | `AddressInput` | *required* | Recipients |
|
|
744
|
-
| `cc` | `AddressInput` | — | CC recipients (visible in headers) |
|
|
745
|
-
| `bcc` | `AddressInput` | — | BCC recipients (envelope only, not in headers) |
|
|
746
|
-
| `replyTo` | `AddressInput` | — | Reply-To header |
|
|
747
|
-
| `subject` | `string` | *required* | Email subject (RFC 2047 for non-ASCII) |
|
|
748
|
-
| `text` | `string` | — | Plain text body |
|
|
749
|
-
| `html` | `string` | — | HTML body |
|
|
750
|
-
| `attachments` | `Attachment[]` | — | File attachments |
|
|
751
|
-
| `headers` | `Record<string, string>` | — | Custom headers |
|
|
752
|
-
| `messageId` | `string` | auto | Message-ID header |
|
|
753
|
-
| `idempotencyKey` | `string` | — | Dedupe key for retry/replay (Resend sends as `Idempotency-Key` header) |
|
|
754
|
-
| `react` | `unknown` | — | React element — use with `reactPlugin()` from `sently/react` |
|
|
755
|
-
| `date` | `Date` | now | Date header |
|
|
756
|
-
| `priority` | `'high' \| 'normal' \| 'low'` | — | X-Priority / Importance |
|
|
757
|
-
| `encoding` | `'utf-8' \| 'ascii'` | `'utf-8'` | Character encoding hint |
|
|
758
|
-
|
|
759
|
-
---
|
|
760
|
-
|
|
761
|
-
## Attachments
|
|
762
|
-
|
|
763
|
-
### In-memory (all runtimes)
|
|
764
|
-
|
|
765
|
-
```typescript
|
|
766
|
-
await mailer.send({
|
|
767
|
-
from: "you@example.com",
|
|
768
|
-
to: "user@example.com",
|
|
769
|
-
subject: "With attachment",
|
|
770
|
-
text: "See attached",
|
|
771
|
-
attachments: [
|
|
772
|
-
{
|
|
773
|
-
filename: "report.pdf",
|
|
774
|
-
content: pdfBytes, // Uint8Array
|
|
775
|
-
contentType: "application/pdf",
|
|
776
|
-
},
|
|
777
|
-
],
|
|
778
|
-
});
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
### File path (Node.js / Bun / Deno only)
|
|
782
|
-
|
|
783
|
-
`attachment.path` reads from disk — see [Security](#security) (Attachments) for validation and `basePath`.
|
|
784
|
-
|
|
785
|
-
```typescript
|
|
786
|
-
attachments: [
|
|
787
|
-
{
|
|
788
|
-
filename: "report.pdf",
|
|
789
|
-
path: "/path/to/report.pdf",
|
|
790
|
-
},
|
|
791
|
-
],
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
On Cloudflare Workers and browsers, use `content: Uint8Array` — `attachment.path` is not supported.
|
|
795
|
-
|
|
796
|
-
---
|
|
797
|
-
|
|
798
|
-
## Error Handling
|
|
799
|
-
|
|
800
|
-
All transport errors extend `SentlyError` for unified handling while preserving existing class names and properties:
|
|
801
|
-
|
|
802
|
-
```typescript
|
|
803
|
-
import { SentlyError } from "sently/errors";
|
|
804
|
-
import { SMTPError } from "sently/transports/smtp";
|
|
805
|
-
import { ResendError } from "sently/transports/resend";
|
|
806
|
-
// Each HTTP transport exports its own error class:
|
|
807
|
-
// SendGridError → sently/transports/sendgrid
|
|
808
|
-
// PostmarkError → sently/transports/postmark
|
|
809
|
-
// MailgunError → sently/transports/mailgun
|
|
810
|
-
// SESError → sently/transports/ses
|
|
811
|
-
// BrevoError → sently/transports/brevo
|
|
812
|
-
// CloudflareEmailError → sently/transports/cloudflare-email
|
|
813
|
-
// FallbackError → sently/transports/fallback (all providers failed; see .attempts)
|
|
814
|
-
|
|
815
|
-
try {
|
|
816
|
-
await mailer.send({ ... });
|
|
817
|
-
} catch (err) {
|
|
818
|
-
if (err instanceof SentlyError) {
|
|
819
|
-
console.error(err.sentlyCode); // e.g. "BAD_REQUEST", "RATE_LIMITED"
|
|
820
|
-
console.error(err.statusCode); // HTTP status when applicable
|
|
821
|
-
}
|
|
822
|
-
if (err instanceof SMTPError) {
|
|
823
|
-
console.error(err.code); // SMTP response code, e.g. 550 (numeric)
|
|
824
|
-
console.error(err.command); // failed command, e.g. "RCPT TO"
|
|
825
|
-
}
|
|
826
|
-
if (err instanceof ResendError) {
|
|
827
|
-
console.error(err.statusCode); // HTTP status code
|
|
828
|
-
console.error(err.code); // machine-readable, e.g. "BAD_REQUEST"
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
```
|
|
832
|
-
|
|
833
|
-
Use `sentlyCode` for unified machine-readable codes when a subclass shadows `code` (e.g. SMTP numeric codes, Brevo/SES provider API codes). Import error classes from their transport subpath — HTTP failures also expose `statusCode`.
|
|
834
|
-
|
|
835
|
-
---
|
|
836
|
-
|
|
837
|
-
## Security
|
|
838
|
-
|
|
839
|
-
sently is built to be secure by default — protections are enforced at the library's core chokepoints, so they apply to every transport and every address field without any extra configuration.
|
|
840
|
-
|
|
841
|
-
### Email header & SMTP command injection
|
|
842
|
-
|
|
843
|
-
All addresses **and** display names are validated centrally in `parseAddresses()` (and re-asserted when rendering headers), before any normalization:
|
|
844
|
-
|
|
845
|
-
- Rejects CR, LF, NUL, every other C0 control (`0x00`–`0x1F`), DEL (`0x7F`), and the Unicode line/paragraph separators `U+2028`/`U+2029`.
|
|
846
|
-
- **Fails closed:** hostile input throws a clear error (with the offending code point) — it is never stripped, repaired, and then accepted.
|
|
847
|
-
- Protects the **display name** too, so an ASCII name like `"Foo\r\nBcc: attacker@evil.com"` can no longer inject a header.
|
|
848
|
-
- Enforced consistently across **From, To, Cc, Bcc, and Reply-To**, and across **every transport** (SMTP, SES, Mailgun, Postmark, Resend, SendGrid, Brevo).
|
|
849
|
-
|
|
850
|
-
```typescript
|
|
851
|
-
await mailer.send({
|
|
852
|
-
from: "you@example.com",
|
|
853
|
-
to: { address: "victim@x.com\r\nBcc: attacker@evil.com" },
|
|
854
|
-
subject: "Hi",
|
|
855
|
-
text: "...",
|
|
856
|
-
});
|
|
857
|
-
// → throws: Email address contains a forbidden control character (0x0d)
|
|
858
|
-
```
|
|
859
|
-
|
|
860
|
-
MIME attachment filenames and custom attachment headers are likewise sanitized against header injection.
|
|
861
|
-
|
|
862
|
-
### Credential protection
|
|
863
|
-
|
|
864
|
-
- **`requireTLS`** (default `true` when `auth` is set) refuses to authenticate over a cleartext connection, defeating STARTTLS-stripping downgrade attacks.
|
|
865
|
-
- **OAuth2 / XOAUTH2** and DKIM signing are built in via Web Crypto — no plaintext secrets in transit beyond what the protocol requires.
|
|
866
|
-
|
|
867
|
-
### Attachments
|
|
868
|
-
|
|
869
|
-
> ⚠️ `attachment.path` reads files from disk. Never pass user-controlled paths without validation.
|
|
870
|
-
|
|
871
|
-
`resolveAttachments()` accepts an opt-in `basePath` that confines reads to an allowed directory and rejects path-traversal (including sibling-directory prefix tricks like `/var/data-secret` vs `/var/data`). Note: `basePath` does not dereference symlinks — use `fs.realpath()` first if symlink traversal is a concern.
|
|
872
|
-
|
|
873
|
-
### Supply chain
|
|
874
|
-
|
|
875
|
-
**Zero runtime dependencies** — there is no transitive dependency tree to audit or to be compromised.
|
|
876
|
-
|
|
877
|
-
---
|
|
878
|
-
|
|
879
|
-
## Bundle size
|
|
880
|
-
|
|
881
|
-
Sizes are **minified + gzip** per import path (`bun run measure:size`; CI: `bun run check:size`). Node built-ins and `cloudflare:sockets` are external.
|
|
882
|
-
|
|
883
|
-
Nodemailer ships **~58 KB gzip** regardless of transport ([BundlePhobia, v8.0.10](https://bundlephobia.com/package/nodemailer@8.0.10)). sently tree-shakes by subpath — pick the entry that matches how you send:
|
|
884
|
-
|
|
885
|
-
| How you send | Import | ~gzip |
|
|
886
|
-
|--------------|--------|-------|
|
|
887
|
-
| HTTP API (Resend, SendGrid, …) | `sently/mailer` + `sently/transports/<provider>` | **~6.1 KB** |
|
|
888
|
-
| SMTP relay (`host` / `port`) | `sently/smtp` (or `createSMTPMailer` from `sently`) | **~15 KB** |
|
|
889
|
-
| Transport only (no mailer wrapper) | `sently/transports/<provider>` | **~4.7 KB** |
|
|
890
|
-
|
|
891
|
-
Regenerate full tables with `bun run measure:size:md`. Measured **2026-05-31** (minified + gzip):
|
|
892
|
-
|
|
893
|
-
#### Common stacks
|
|
894
|
-
|
|
895
|
-
| What | Imports | ~gzip |
|
|
896
|
-
|------|---------|-------|
|
|
897
|
-
| HTTP — Resend | `sently/mailer` + `sently/transports/resend` | ~6.1 KB |
|
|
898
|
-
| HTTP — SendGrid | `sently/mailer` + `sently/transports/sendgrid` | ~5.9 KB |
|
|
899
|
-
| HTTP — transport only | `sently/transports/resend` (no `createMailer` wrapper) | ~4.7 KB |
|
|
900
|
-
| SMTP relay | `sently/smtp` with `{ host, port, auth }` | ~14.8 KB |
|
|
901
|
-
| SMTP + Node adapter | `sently/smtp` + `sently/adapters/node` | ~14.8 KB |
|
|
902
|
-
| Main entry + HTTP | `sently` + HTTP transport via main `createMailer` | ~6.1 KB |
|
|
903
|
-
|
|
904
|
-
#### Core entries
|
|
905
|
-
|
|
906
|
-
| What | Imports | ~gzip |
|
|
907
|
-
|------|---------|-------|
|
|
908
|
-
| sently/mailer | Transport-only `createMailer` (plugins, sendBulk) | ~2.6 KB |
|
|
909
|
-
| sently | Main entry — types, factories, OAuth2, `SentlyError` | ~2.6 KB |
|
|
910
|
-
| sently/smtp | SMTP `createSMTPMailer` — host/port, pool, adapters | ~14.7 KB |
|
|
911
|
-
|
|
912
|
-
```ts
|
|
913
|
-
// HTTP
|
|
914
|
-
import { createMailer } from "sently/mailer";
|
|
915
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
916
|
-
|
|
917
|
-
// SMTP
|
|
918
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
919
|
-
```
|
|
920
|
-
|
|
921
|
-
Main `"sently"` exports shared types, `createMailer`, `createSMTPMailer`, `detectRuntime`, OAuth2, `SentlyError`, `consoleObserver`, and the v0.8 HTTP providers (`LoopsTransport`, `MailerSendTransport`, …), plus `FallbackTransport`, `WeightedFallbackTransport`, and `CloudflareEmailTransport`. Webhooks, idempotency, DKIM, SMTP-only transports, and plugins remain separate subpaths for smallest bundles.
|
|
922
|
-
|
|
923
|
-
---
|
|
924
|
-
|
|
925
|
-
## Choosing an entrypoint
|
|
926
|
-
|
|
927
|
-
```
|
|
928
|
-
How do you send mail?
|
|
929
|
-
│
|
|
930
|
-
├─ HTTP API (Resend, SendGrid, …)
|
|
931
|
-
│ import { createMailer } from "sently/mailer"
|
|
932
|
-
│ import { ResendTransport } from "sently/transports/resend"
|
|
933
|
-
│ createMailer({ transport: new ResendTransport({ apiKey }) })
|
|
934
|
-
│
|
|
935
|
-
├─ SMTP relay (host / port / auth)
|
|
936
|
-
│ import { createSMTPMailer } from "sently/smtp"
|
|
937
|
-
│ createSMTPMailer({ host, port, auth })
|
|
938
|
-
│
|
|
939
|
-
├─ Provider failover / weighted routing
|
|
940
|
-
│ import { FallbackTransport } from "sently/transports/fallback"
|
|
941
|
-
│ import { WeightedFallbackTransport } from "sently/transports/weighted-fallback"
|
|
942
|
-
│ createMailer({ transport: new FallbackTransport([primary, backup]) })
|
|
943
|
-
│
|
|
944
|
-
└─ Custom / decorated transport (Retry, Idempotency, Preview)
|
|
945
|
-
import { createMailer } from "sently/mailer"
|
|
946
|
-
createMailer({ transport: new RetryTransport(inner) })
|
|
947
|
-
```
|
|
948
|
-
|
|
949
|
-
---
|
|
950
|
-
|
|
951
|
-
## Migrating from Nodemailer
|
|
952
|
-
|
|
953
|
-
| Nodemailer | sently |
|
|
954
|
-
|------------|--------|
|
|
955
|
-
| `nodemailer.createTransport({...})` | `await createSMTPMailer({...})` or `createMailer({ transport })` |
|
|
956
|
-
| `transporter.sendMail(options)` | `mailer.send(options)` |
|
|
957
|
-
| `transporter.verify()` | `mailer.verify()` |
|
|
958
|
-
| `options.attachments[].path` | Same (Node/Bun/Deno); use `content` on edge |
|
|
959
|
-
| `import nodemailer from 'nodemailer'` | `import { createMailer } from 'sently/mailer'` (HTTP) or `createSMTPMailer` from `'sently/smtp'` |
|
|
960
|
-
| CommonJS | ESM only |
|
|
961
|
-
| Node.js only | Node, Bun, Deno, CF Workers |
|
|
962
|
-
|
|
963
|
-
---
|
|
964
|
-
|
|
965
|
-
## TypeScript
|
|
966
|
-
|
|
967
|
-
```typescript
|
|
968
|
-
import type {
|
|
969
|
-
MailOptions,
|
|
970
|
-
MailPlugin,
|
|
971
|
-
SendResult,
|
|
972
|
-
Attachment,
|
|
973
|
-
SMTPConfig,
|
|
974
|
-
SMTPMailerOptions,
|
|
975
|
-
TransportMailerOptions,
|
|
976
|
-
} from "sently";
|
|
977
|
-
```
|
|
83
|
+
| | Nodemailer | sently |
|
|
84
|
+
| ----------------- | ----------------------- | ------------------------------------------- |
|
|
85
|
+
| Bundle size | ~59 KB gzip always | ~6.3 KB HTTP · ~14.9 KB SMTP |
|
|
86
|
+
| Runtimes | Node.js only | Node, Bun, Deno, CF Workers |
|
|
87
|
+
| Module format | CommonJS | ESM only |
|
|
88
|
+
| Dependencies | 0 | 0 |
|
|
89
|
+
| Channels | Email | Email · SMS · WhatsApp · Push |
|
|
90
|
+
| HTTP transports | via plugins | built-in subpaths |
|
|
91
|
+
| Provider failover | — | `FallbackTransport` + weighted routing |
|
|
92
|
+
| TypeScript | `@types/nodemailer` | built-in |
|
|
978
93
|
|
|
979
|
-
|
|
94
|
+
## Entrypoints
|
|
980
95
|
|
|
981
|
-
|
|
96
|
+
| Import | Use when |
|
|
97
|
+
| ---------------------- | --------------------------------------------- |
|
|
98
|
+
| `sently/mailer` | HTTP / custom email transports (smallest) |
|
|
99
|
+
| `sently/smtp` | SMTP host, pool, adapters, DKIM |
|
|
100
|
+
| `sently/sms` | SMS |
|
|
101
|
+
| `sently/whatsapp` | WhatsApp |
|
|
102
|
+
| `sently/push` | Web Push |
|
|
103
|
+
| `sently/transports/*` | One provider per subpath |
|
|
982
104
|
|
|
983
|
-
##
|
|
105
|
+
## Documentation
|
|
984
106
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
107
|
+
| Resource | Link |
|
|
108
|
+
| ------------ | ------------------------------------------------------------ |
|
|
109
|
+
| Docs site | [sently.omqkhafi.dev](https://sently.omqkhafi.dev) |
|
|
110
|
+
| Handbook | [/docs](https://sently.omqkhafi.dev/docs) |
|
|
111
|
+
| Get started | [/docs/get-started](https://sently.omqkhafi.dev/docs/get-started) |
|
|
112
|
+
| Channels | [/docs/channels](https://sently.omqkhafi.dev/docs/channels) |
|
|
113
|
+
| Transports | [/docs/transports](https://sently.omqkhafi.dev/docs/transports) |
|
|
114
|
+
| Agents index | [/llms.txt](https://sently.omqkhafi.dev/llms.txt) |
|
|
115
|
+
| Changelog | [`CHANGELOG.md`](CHANGELOG.md) |
|
|
116
|
+
| Agents | [`AGENTS.md`](AGENTS.md) |
|
|
988
117
|
|
|
989
|
-
|
|
118
|
+
Local docs: `bun run site:dev`. Verify: `bun run verify`.
|
|
990
119
|
|
|
991
|
-
MIT
|
|
120
|
+
Pre-1.0. Published on [npm](https://www.npmjs.com/package/sently) and [JSR](https://jsr.io/@alialnaghmoush/sently). MIT.
|