sently 0.7.2 → 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 +49 -1
- package/README.md +89 -776
- 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-0gqy32pe.js.map +10 -0
- package/dist/{chunk-kebhy657.js → chunk-0qxws3kj.js} +2 -2
- package/dist/{chunk-kebhy657.js.map → chunk-0qxws3kj.js.map} +1 -1
- package/dist/chunk-29z7fkzy.js +4 -0
- package/dist/{chunk-74rha3ac.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-keksa6fx.js → chunk-6npp3x3c.js} +2 -2
- package/dist/{chunk-keksa6fx.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-gkkv7yqm.js → chunk-8dty8c4p.js} +3 -3
- package/dist/{chunk-gkkv7yqm.js.map → chunk-8dty8c4p.js.map} +2 -2
- package/dist/{chunk-qp4y0kp2.js → chunk-8njyga62.js} +2 -2
- package/dist/{chunk-qp4y0kp2.js.map → chunk-8njyga62.js.map} +1 -1
- package/dist/{chunk-gh5n759p.js → chunk-93vqxxj2.js} +3 -3
- package/dist/{chunk-gh5n759p.js.map → chunk-93vqxxj2.js.map} +1 -1
- package/dist/chunk-b602dhck.js +4 -0
- package/dist/{chunk-rm78s1ek.js.map → chunk-b602dhck.js.map} +2 -2
- package/dist/chunk-gp4fwrs8.js +4 -0
- package/dist/chunk-gp4fwrs8.js.map +10 -0
- package/dist/chunk-n0qeyzqm.js +13 -0
- package/dist/{chunk-cvrbfke9.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-r7ee9tk7.js.map → chunk-skqhj0rm.js.map} +2 -2
- package/dist/{chunk-a1c87hn1.js → chunk-tamww15j.js} +2 -2
- package/dist/{chunk-a1c87hn1.js.map → chunk-tamww15j.js.map} +1 -1
- package/dist/chunk-vjpds2pf.js +3 -0
- package/dist/chunk-vjpds2pf.js.map +10 -0
- 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-d5rwm3br.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/provider-label.d.ts +6 -0
- 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/types.d.ts +27 -4
- 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.d.ts +1 -0
- package/dist/idempotency.js +2 -2
- package/dist/idempotency.js.map +3 -3
- package/dist/index.d.ts +24 -2
- package/dist/index.js +23 -0
- package/dist/mailer.js +2 -2
- package/dist/mailer.js.map +1 -1
- package/dist/observability/console.d.ts +12 -0
- package/dist/observability/console.js +3 -0
- package/dist/observability/console.js.map +10 -0
- 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.d.ts +1 -0
- package/dist/transports/brevo.js +2 -2
- package/dist/transports/brevo.js.map +3 -3
- package/dist/transports/cloudflare-email.d.ts +58 -0
- package/dist/transports/cloudflare-email.js +3 -0
- package/dist/transports/cloudflare-email.js.map +10 -0
- package/dist/transports/fallback.d.ts +100 -0
- package/dist/transports/fallback.js +3 -0
- package/dist/transports/fallback.js.map +9 -0
- package/dist/transports/loops.d.ts +32 -0
- package/dist/transports/loops.js +3 -0
- package/dist/transports/loops.js.map +10 -0
- package/dist/transports/mailersend.d.ts +27 -0
- package/dist/transports/mailersend.js +3 -0
- package/dist/transports/mailersend.js.map +10 -0
- package/dist/transports/mailgun.d.ts +1 -0
- package/dist/transports/mailgun.js +2 -2
- package/dist/transports/mailgun.js.map +3 -3
- package/dist/transports/mailtrap.d.ts +36 -0
- package/dist/transports/mailtrap.js +3 -0
- package/dist/transports/mailtrap.js.map +10 -0
- 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.d.ts +29 -0
- package/dist/transports/plunk.js +3 -0
- package/dist/transports/plunk.js.map +10 -0
- package/dist/transports/postmark.d.ts +1 -0
- package/dist/transports/postmark.js +2 -2
- package/dist/transports/postmark.js.map +3 -3
- package/dist/transports/preview.d.ts +1 -0
- package/dist/transports/preview.js +2 -2
- package/dist/transports/preview.js.map +3 -3
- package/dist/transports/resend.d.ts +1 -0
- package/dist/transports/resend.js +2 -2
- package/dist/transports/resend.js.map +3 -3
- package/dist/transports/retry.d.ts +1 -0
- package/dist/transports/retry.js +2 -2
- package/dist/transports/retry.js.map +3 -3
- package/dist/transports/sendgrid.d.ts +1 -0
- package/dist/transports/sendgrid.js +2 -2
- package/dist/transports/sendgrid.js.map +3 -3
- package/dist/transports/ses.d.ts +1 -0
- package/dist/transports/ses.js +3 -3
- package/dist/transports/ses.js.map +3 -3
- package/dist/transports/smtp.d.ts +1 -0
- package/dist/transports/smtp.js +2 -2
- package/dist/transports/smtp.js.map +1 -1
- package/dist/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.d.ts +30 -0
- package/dist/transports/sparkpost.js +3 -0
- package/dist/transports/sparkpost.js.map +10 -0
- 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.d.ts +65 -0
- package/dist/transports/weighted-fallback.js +3 -0
- package/dist/transports/weighted-fallback.js.map +10 -0
- 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 +121 -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-4mvp3b85.js +0 -3
- package/dist/chunk-4mvp3b85.js.map +0 -10
- package/dist/chunk-74rha3ac.js +0 -4
- package/dist/chunk-cvrbfke9.js +0 -13
- package/dist/chunk-d5rwm3br.js +0 -5
- package/dist/chunk-ka6t8g77.js +0 -4
- package/dist/chunk-ka6t8g77.js.map +0 -11
- package/dist/chunk-r7ee9tk7.js +0 -4
- package/dist/chunk-rm78s1ek.js +0 -4
package/README.md
CHANGED
|
@@ -1,136 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## The 30-second tour
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
import type { MailOptions } from "sently";
|
|
49
|
-
import { createMailer } from "sently/mailer";
|
|
50
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
51
|
-
import { PreviewTransport } from "sently/transports/preview";
|
|
52
|
-
|
|
53
|
-
const addFooter = (options: MailOptions): MailOptions => ({
|
|
54
|
-
...options,
|
|
55
|
-
html: (options.html ?? "") + '<p style="color:#999">Unsubscribe</p>',
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Swap providers without changing send code
|
|
59
|
-
const mailer = await createMailer({
|
|
60
|
-
transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
|
|
61
|
-
plugins: [addFooter],
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
await mailer.send({
|
|
65
|
-
from: "you@example.com",
|
|
66
|
-
to: "recipient@example.com",
|
|
67
|
-
subject: "Hello from sently",
|
|
68
|
-
html: "<p>Hello!</p>",
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Bulk send with concurrency control
|
|
72
|
-
await mailer.sendBulk(recipients, { concurrency: 5 });
|
|
73
|
-
|
|
74
|
-
// Local dev — write to disk instead of sending
|
|
75
|
-
const devMailer = await createMailer({
|
|
76
|
-
transport: process.env.CI
|
|
77
|
-
? new ResendTransport({ apiKey: process.env.RESEND_API_KEY! })
|
|
78
|
-
: new PreviewTransport({ outDir: ".emails", open: true }),
|
|
79
|
-
});
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Installation
|
|
85
|
-
|
|
86
|
-
**npm** ([sently](https://www.npmjs.com/package/sently)):
|
|
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
|
|
87
42
|
|
|
88
43
|
```bash
|
|
89
|
-
bun add sently
|
|
90
|
-
|
|
91
|
-
pnpm add sently
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**JSR** ([@alialnaghmoush/sently](https://jsr.io/@alialnaghmoush/sently)) — Deno, Bun, and other JSR-aware runtimes:
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
deno add jsr:@alialnaghmoush/sently
|
|
98
|
-
bunx jsr add @alialnaghmoush/sently
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
import { createMailer } from "sently/mailer"; // HTTP stack ~6.1 KB with a transport
|
|
103
|
-
import { createSMTPMailer } from "sently/smtp"; // SMTP relay ~15 KB
|
|
104
|
-
// Or: import { createSMTPMailer } from "sently";
|
|
44
|
+
bun add sently # npm / Bun / yarn / pnpm
|
|
45
|
+
bunx jsr add @alialnaghmoush/sently # JSR
|
|
105
46
|
```
|
|
106
47
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
## Quick Start
|
|
110
|
-
|
|
111
|
-
### SMTP with auto-detected adapter
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
115
|
-
|
|
116
|
-
const mailer = await createSMTPMailer({
|
|
117
|
-
host: "smtp.example.com",
|
|
118
|
-
port: 587,
|
|
119
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
await mailer.send({
|
|
123
|
-
from: "you@example.com",
|
|
124
|
-
to: "recipient@example.com",
|
|
125
|
-
subject: "Hello from sently",
|
|
126
|
-
text: "Plain text body",
|
|
127
|
-
html: "<p>HTML body</p>",
|
|
128
|
-
});
|
|
48
|
+
Optional peers (React Email only): `react`, `@react-email/render`.
|
|
129
49
|
|
|
130
|
-
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### Resend HTTP transport (Vercel Edge compatible)
|
|
50
|
+
## Quick start
|
|
134
51
|
|
|
135
52
|
```typescript
|
|
136
53
|
import { createMailer } from "sently/mailer";
|
|
@@ -141,667 +58,63 @@ const mailer = await createMailer({
|
|
|
141
58
|
});
|
|
142
59
|
|
|
143
60
|
await mailer.send({
|
|
144
|
-
from: "
|
|
145
|
-
to: "recipient@example.com",
|
|
146
|
-
subject: "Hello from the edge",
|
|
147
|
-
html: "<p>Sent via Resend + sently</p>",
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Cloudflare Worker
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
155
|
-
import { CloudflareAdapter } from "sently/adapters/cf";
|
|
156
|
-
|
|
157
|
-
export default {
|
|
158
|
-
async fetch() {
|
|
159
|
-
const mailer = await createSMTPMailer({
|
|
160
|
-
host: "smtp.example.com",
|
|
161
|
-
port: 587,
|
|
162
|
-
auth: { user: "relay@example.com", pass: "secret" },
|
|
163
|
-
adapter: new CloudflareAdapter(),
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
await mailer.send({
|
|
167
|
-
from: "relay@example.com",
|
|
168
|
-
to: "user@example.com",
|
|
169
|
-
subject: "From a Worker",
|
|
170
|
-
text: "Hello from Cloudflare Workers",
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
return new Response("Sent");
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## Adapters
|
|
181
|
-
|
|
182
|
-
| Runtime | Import | Notes |
|
|
183
|
-
|---------|--------|-------|
|
|
184
|
-
| Node.js (auto) | `createSMTPMailer` from `sently/smtp` | Auto-detected adapter |
|
|
185
|
-
| Node.js (explicit) | `sently/adapters/node` → `NodeAdapter` | Reference implementation |
|
|
186
|
-
| Bun (auto) | `createSMTPMailer` from `sently/smtp` | Auto-detected adapter |
|
|
187
|
-
| Bun (explicit) | `sently/adapters/bun` → `BunAdapter` | Node compat layer |
|
|
188
|
-
| Deno | `sently/adapters/deno` → `DenoAdapter` | Native `Deno.startTls` |
|
|
189
|
-
| Cloudflare Workers | `sently/adapters/cf` → `CloudflareAdapter` | `cloudflare:sockets` |
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
193
|
-
import { NodeAdapter } from "sently/adapters/node";
|
|
194
|
-
|
|
195
|
-
const mailer = await createSMTPMailer({
|
|
196
|
-
host: "smtp.example.com",
|
|
197
|
-
adapter: new NodeAdapter({ secure: false }),
|
|
198
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
199
|
-
});
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## Transports
|
|
205
|
-
|
|
206
|
-
### SMTP
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
import { createMailer } from "sently/mailer";
|
|
210
|
-
import { SMTPTransport } from "sently/transports/smtp";
|
|
211
|
-
import { NodeAdapter } from "sently/adapters/node";
|
|
212
|
-
|
|
213
|
-
const transport = new SMTPTransport({
|
|
214
|
-
host: "smtp.example.com",
|
|
215
|
-
port: 587,
|
|
216
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
217
|
-
adapter: new NodeAdapter(),
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const mailer = await createMailer({ transport });
|
|
221
|
-
await mailer.verify(); // test connection + auth
|
|
222
|
-
```
|
|
223
|
-
|
|
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.
|
|
225
|
-
|
|
226
|
-
**AUTH methods:** XOAUTH2, CRAM-MD5, LOGIN, and PLAIN (auto-negotiated from EHLO unless `auth.type` is set).
|
|
227
|
-
|
|
228
|
-
**`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).
|
|
229
|
-
|
|
230
|
-
#### DKIM signing
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
234
|
-
|
|
235
|
-
const mailer = await createSMTPMailer({
|
|
236
|
-
host: "smtp.example.com",
|
|
237
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
238
|
-
dkim: {
|
|
239
|
-
domainName: "example.com",
|
|
240
|
-
keySelector: "2024",
|
|
241
|
-
privateKey: await Bun.file("dkim-private.pem").text(),
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
Pass `dkim` on SMTP config or use `signDKIM` from `sently/dkim` directly. MIME lazy-loads DKIM only when the option is set.
|
|
247
|
-
|
|
248
|
-
#### Gmail OAuth2 (XOAUTH2)
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
252
|
-
|
|
253
|
-
const mailer = await createSMTPMailer({
|
|
254
|
-
host: "smtp.gmail.com",
|
|
255
|
-
port: 465,
|
|
256
|
-
secure: true,
|
|
257
|
-
auth: {
|
|
258
|
-
type: "OAUTH2",
|
|
259
|
-
user: "me@gmail.com",
|
|
260
|
-
oauth2: {
|
|
261
|
-
user: "me@gmail.com",
|
|
262
|
-
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
263
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
264
|
-
refreshToken: process.env.GOOGLE_REFRESH_TOKEN!,
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
```
|
|
269
|
-
|
|
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
|
-
```
|
|
292
|
-
|
|
293
|
-
#### Connection pooling
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
297
|
-
|
|
298
|
-
const mailer = await createSMTPMailer({
|
|
299
|
-
host: "smtp.example.com",
|
|
300
|
-
pool: true,
|
|
301
|
-
maxConnections: 5,
|
|
302
|
-
maxMessages: 100,
|
|
303
|
-
rateDelta: 10,
|
|
304
|
-
rateLimit: 1000,
|
|
305
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
306
|
-
});
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
Or use `SMTPPool` directly:
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
import { SMTPPool } from "sently/pool";
|
|
313
|
-
|
|
314
|
-
const pool = new SMTPPool({
|
|
315
|
-
host: "smtp.example.com",
|
|
316
|
-
adapter: new NodeAdapter(),
|
|
317
|
-
auth: { user: "you@example.com", pass: "secret" },
|
|
318
|
-
});
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### HTTP APIs
|
|
322
|
-
|
|
323
|
-
| Transport | Import path | Required config |
|
|
324
|
-
|-----------|-------------|-----------------|
|
|
325
|
-
| Mailer wrapper | `sently/mailer` | — (use with any transport below) |
|
|
326
|
-
| Resend | `sently/transports/resend` | `apiKey` |
|
|
327
|
-
| SendGrid | `sently/transports/sendgrid` | `apiKey` |
|
|
328
|
-
| Postmark | `sently/transports/postmark` | `serverToken` |
|
|
329
|
-
| Mailgun | `sently/transports/mailgun` | `apiKey`, `domain` |
|
|
330
|
-
| AWS SES | `sently/transports/ses` | `accessKeyId`, `secretAccessKey`, `region` |
|
|
331
|
-
| Brevo | `sently/transports/brevo` | `apiKey` |
|
|
332
|
-
|
|
333
|
-
All transports implement the same interface — swap without changing your send code.
|
|
334
|
-
|
|
335
|
-
Messages with attachments are sent as raw MIME (`Content.Raw`); simple messages use `Content.Simple`.
|
|
336
|
-
|
|
337
|
-
### PreviewTransport
|
|
338
|
-
|
|
339
|
-
Write emails to disk during local development instead of sending them:
|
|
340
|
-
|
|
341
|
-
```typescript
|
|
342
|
-
import { PreviewTransport } from "sently/transports/preview";
|
|
343
|
-
import { createMailer } from "sently/mailer";
|
|
344
|
-
|
|
345
|
-
const mailer = await createMailer({
|
|
346
|
-
transport: new PreviewTransport({
|
|
347
|
-
outDir: "./.emails",
|
|
348
|
-
open: true,
|
|
349
|
-
format: "html",
|
|
350
|
-
}),
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
await mailer.send({
|
|
354
|
-
from: "dev@localhost",
|
|
61
|
+
from: "hello@example.com",
|
|
355
62
|
to: "you@example.com",
|
|
356
|
-
subject: "Preview me",
|
|
357
|
-
html: "<h1>Hello</h1>",
|
|
358
|
-
});
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### RetryTransport
|
|
362
|
-
|
|
363
|
-
Wrap any transport with automatic retries and configurable backoff:
|
|
364
|
-
|
|
365
|
-
```typescript
|
|
366
|
-
import { RetryTransport } from "sently/transports/retry";
|
|
367
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
368
|
-
import { createMailer } from "sently/mailer";
|
|
369
|
-
|
|
370
|
-
const transport = new RetryTransport(
|
|
371
|
-
new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
|
|
372
|
-
{ maxAttempts: 3, backoff: "exponential", retryOn: [429, 503] },
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
const mailer = await createMailer({ transport });
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
### sendBulk()
|
|
379
|
-
|
|
380
|
-
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.
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
const result = await mailer.sendBulk(
|
|
384
|
-
[
|
|
385
|
-
{ from: "a@b.com", to: "1@example.com", subject: "One", text: "Hi" },
|
|
386
|
-
{ from: "a@b.com", to: "2@example.com", subject: "Two", text: "Hi" },
|
|
387
|
-
],
|
|
388
|
-
{
|
|
389
|
-
concurrency: 2,
|
|
390
|
-
stopOnError: false, // halt remaining sends after first failure when true
|
|
391
|
-
onSuccess: (_msg, index) => console.log(`Sent #${index}`),
|
|
392
|
-
onError: (_msg, index, err) => console.error(`Failed #${index}`, err),
|
|
393
|
-
},
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
console.log(result.sent, result.failed);
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
Resend batches up to `RESEND_BATCH_MAX` (100) messages per request — export from `sently/transports/resend`.
|
|
400
|
-
|
|
401
|
-
### Mailer lifecycle hooks
|
|
402
|
-
|
|
403
|
-
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.
|
|
404
|
-
|
|
405
|
-
```typescript
|
|
406
|
-
const mailer = await createMailer({
|
|
407
|
-
transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
|
|
408
|
-
hooks: {
|
|
409
|
-
onSend: (ctx) => metrics.increment("email.send", { provider: ctx.provider }),
|
|
410
|
-
onSuccess: (ctx, result) => metrics.increment("email.success"),
|
|
411
|
-
onError: (ctx, err) => metrics.increment("email.error"),
|
|
412
|
-
onRetry: (ctx, attempt, err) => metrics.increment("email.retry", { attempt }),
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
```
|
|
416
|
-
|
|
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.
|
|
418
|
-
|
|
419
|
-
Works with both `sently/mailer` (`{ transport, hooks }`) and SMTP config via `createSMTPMailer` (`{ host, auth, hooks }`).
|
|
420
|
-
|
|
421
|
-
### IdempotencyTransport
|
|
422
|
-
|
|
423
|
-
Prevent duplicate sends on retry or replay. Wrap **outside** `RetryTransport` so all retry attempts share one key:
|
|
424
|
-
|
|
425
|
-
```typescript
|
|
426
|
-
import { IdempotencyTransport } from "sently/idempotency";
|
|
427
|
-
import { RetryTransport } from "sently/transports/retry";
|
|
428
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
429
|
-
|
|
430
|
-
const transport = new IdempotencyTransport(
|
|
431
|
-
new RetryTransport(new ResendTransport({ apiKey: process.env.RESEND_API_KEY! })),
|
|
432
|
-
{ ttlMs: 86_400_000 },
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
await mailer.send({
|
|
436
|
-
from: "you@example.com",
|
|
437
|
-
to: "user@example.com",
|
|
438
|
-
subject: "Hello",
|
|
439
|
-
text: "Hi",
|
|
440
|
-
idempotencyKey: "order-123-email", // or derive from messageId
|
|
441
|
-
});
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
Resend sends the `Idempotency-Key` HTTP header natively. Supply a shared store (Redis, Dragonfly) in production — `MemoryIdempotencyStore` is for single-process use.
|
|
445
|
-
|
|
446
|
-
---
|
|
447
|
-
|
|
448
|
-
## Plugin system
|
|
449
|
-
|
|
450
|
-
Plugins transform `MailOptions` before the transport builds and sends the message. They run sequentially — each receives the output of the previous plugin.
|
|
451
|
-
|
|
452
|
-
```typescript
|
|
453
|
-
import type { MailOptions } from "sently";
|
|
454
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
455
|
-
|
|
456
|
-
const addFooter = (options: MailOptions) => ({
|
|
457
|
-
...options,
|
|
458
|
-
html: (options.html ?? "") + '<p style="color:#999">Unsubscribe</p>',
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
const mailer = await createSMTPMailer({
|
|
462
|
-
host: "smtp.resend.com",
|
|
463
|
-
port: 465,
|
|
464
|
-
secure: true,
|
|
465
|
-
auth: { user: "resend", pass: process.env.RESEND_API_KEY! },
|
|
466
|
-
plugins: [addFooter],
|
|
467
|
-
});
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
Works with SMTP config or custom transports:
|
|
471
|
-
|
|
472
|
-
```typescript
|
|
473
|
-
import { createMailer } from "sently/mailer";
|
|
474
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
475
|
-
|
|
476
|
-
const mailer = await createMailer({
|
|
477
|
-
transport: new ResendTransport({ apiKey: "re_..." }),
|
|
478
|
-
plugins: [addFooter],
|
|
479
|
-
});
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
### TemplatePlugin
|
|
483
|
-
|
|
484
|
-
Render HTML from named templates with zero dependencies:
|
|
485
|
-
|
|
486
|
-
```typescript
|
|
487
|
-
import { templatePlugin, simpleEngine } from "sently/plugins/template";
|
|
488
|
-
import { createMailer } from "sently/mailer";
|
|
489
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
490
|
-
|
|
491
|
-
const mailer = await createMailer({
|
|
492
|
-
transport: new ResendTransport({ apiKey: "re_..." }),
|
|
493
|
-
plugins: [
|
|
494
|
-
templatePlugin({
|
|
495
|
-
engine: simpleEngine,
|
|
496
|
-
templates: {
|
|
497
|
-
welcome: "<h1>Hello, {{name}}!</h1>",
|
|
498
|
-
},
|
|
499
|
-
}),
|
|
500
|
-
],
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
await mailer.send({
|
|
504
|
-
from: "onboarding@yourdomain.com",
|
|
505
|
-
to: "user@example.com",
|
|
506
63
|
subject: "Welcome",
|
|
507
|
-
|
|
508
|
-
data: { name: "Ali" },
|
|
509
|
-
});
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
Use a custom engine by passing any `(template, data) => string` function to `templatePlugin`.
|
|
513
|
-
|
|
514
|
-
### React Email plugin
|
|
515
|
-
|
|
516
|
-
Render React Email components to HTML and plain text (optional peers: `react`, `@react-email/render`):
|
|
517
|
-
|
|
518
|
-
```typescript
|
|
519
|
-
import { reactPlugin } from "sently/react";
|
|
520
|
-
import { createMailer } from "sently/mailer";
|
|
521
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
522
|
-
import { WelcomeEmail } from "./emails/welcome";
|
|
523
|
-
|
|
524
|
-
const mailer = await createMailer({
|
|
525
|
-
transport: new ResendTransport({ apiKey: "re_..." }),
|
|
526
|
-
plugins: [reactPlugin()],
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
await mailer.send({
|
|
530
|
-
from: "onboarding@yourdomain.com",
|
|
531
|
-
to: "user@example.com",
|
|
532
|
-
subject: "Welcome",
|
|
533
|
-
react: WelcomeEmail({ name: "Ali" }),
|
|
534
|
-
});
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
Explicit `html` / `text` always win over rendered output.
|
|
538
|
-
|
|
539
|
-
### Webhook parsing
|
|
540
|
-
|
|
541
|
-
Normalize provider webhooks into a single event type — no server framework required:
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
import { parseResendWebhook, parseSesWebhook } from "sently/webhooks";
|
|
545
|
-
|
|
546
|
-
// Resend (Svix-style payload)
|
|
547
|
-
const events = parseResendWebhook(await request.json());
|
|
548
|
-
|
|
549
|
-
// AWS SES via SNS (handles SubscriptionConfirmation + double-encoded Message)
|
|
550
|
-
const sesEvents = parseSesWebhook(await request.json());
|
|
551
|
-
|
|
552
|
-
for (const event of events) {
|
|
553
|
-
console.log(event.type, event.messageId, event.recipient);
|
|
554
|
-
}
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
Parsers: Resend, SendGrid, Postmark, Mailgun, SES, Brevo. Optional HMAC verification helpers for Mailgun and Resend (`verifyMailgunSignature`, `verifyResendSignature`).
|
|
558
|
-
|
|
559
|
-
---
|
|
560
|
-
|
|
561
|
-
## MailOptions Reference
|
|
562
|
-
|
|
563
|
-
| Field | Type | Default | Description |
|
|
564
|
-
|-------|------|---------|-------------|
|
|
565
|
-
| `from` | `AddressInput` | *required* | Sender address |
|
|
566
|
-
| `to` | `AddressInput` | *required* | Recipients |
|
|
567
|
-
| `cc` | `AddressInput` | — | CC recipients (visible in headers) |
|
|
568
|
-
| `bcc` | `AddressInput` | — | BCC recipients (envelope only, not in headers) |
|
|
569
|
-
| `replyTo` | `AddressInput` | — | Reply-To header |
|
|
570
|
-
| `subject` | `string` | *required* | Email subject (RFC 2047 for non-ASCII) |
|
|
571
|
-
| `text` | `string` | — | Plain text body |
|
|
572
|
-
| `html` | `string` | — | HTML body |
|
|
573
|
-
| `attachments` | `Attachment[]` | — | File attachments |
|
|
574
|
-
| `headers` | `Record<string, string>` | — | Custom headers |
|
|
575
|
-
| `messageId` | `string` | auto | Message-ID header |
|
|
576
|
-
| `idempotencyKey` | `string` | — | Dedupe key for retry/replay (Resend sends as `Idempotency-Key` header) |
|
|
577
|
-
| `react` | `unknown` | — | React element — use with `reactPlugin()` from `sently/react` |
|
|
578
|
-
| `date` | `Date` | now | Date header |
|
|
579
|
-
| `priority` | `'high' \| 'normal' \| 'low'` | — | X-Priority / Importance |
|
|
580
|
-
| `encoding` | `'utf-8' \| 'ascii'` | `'utf-8'` | Character encoding hint |
|
|
581
|
-
|
|
582
|
-
---
|
|
583
|
-
|
|
584
|
-
## Attachments
|
|
585
|
-
|
|
586
|
-
### In-memory (all runtimes)
|
|
587
|
-
|
|
588
|
-
```typescript
|
|
589
|
-
await mailer.send({
|
|
590
|
-
from: "you@example.com",
|
|
591
|
-
to: "user@example.com",
|
|
592
|
-
subject: "With attachment",
|
|
593
|
-
text: "See attached",
|
|
594
|
-
attachments: [
|
|
595
|
-
{
|
|
596
|
-
filename: "report.pdf",
|
|
597
|
-
content: pdfBytes, // Uint8Array
|
|
598
|
-
contentType: "application/pdf",
|
|
599
|
-
},
|
|
600
|
-
],
|
|
64
|
+
html: "<p>Sent with sently.</p>",
|
|
601
65
|
});
|
|
602
66
|
```
|
|
603
67
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
`attachment.path` reads from disk — see [Security](#security) (Attachments) for validation and `basePath`.
|
|
607
|
-
|
|
608
|
-
```typescript
|
|
609
|
-
attachments: [
|
|
610
|
-
{
|
|
611
|
-
filename: "report.pdf",
|
|
612
|
-
path: "/path/to/report.pdf",
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
On Cloudflare Workers and browsers, use `content: Uint8Array` — `attachment.path` is not supported.
|
|
618
|
-
|
|
619
|
-
---
|
|
620
|
-
|
|
621
|
-
## Error Handling
|
|
622
|
-
|
|
623
|
-
All transport errors extend `SentlyError` for unified handling while preserving existing class names and properties:
|
|
624
|
-
|
|
625
|
-
```typescript
|
|
626
|
-
import { SentlyError } from "sently/errors";
|
|
627
|
-
import { SMTPError } from "sently/transports/smtp";
|
|
628
|
-
import { ResendError } from "sently/transports/resend";
|
|
629
|
-
// Each HTTP transport exports its own error class:
|
|
630
|
-
// SendGridError → sently/transports/sendgrid
|
|
631
|
-
// PostmarkError → sently/transports/postmark
|
|
632
|
-
// MailgunError → sently/transports/mailgun
|
|
633
|
-
// SESError → sently/transports/ses
|
|
634
|
-
// BrevoError → sently/transports/brevo
|
|
635
|
-
|
|
636
|
-
try {
|
|
637
|
-
await mailer.send({ ... });
|
|
638
|
-
} catch (err) {
|
|
639
|
-
if (err instanceof SentlyError) {
|
|
640
|
-
console.error(err.sentlyCode); // e.g. "BAD_REQUEST", "RATE_LIMITED"
|
|
641
|
-
console.error(err.statusCode); // HTTP status when applicable
|
|
642
|
-
}
|
|
643
|
-
if (err instanceof SMTPError) {
|
|
644
|
-
console.error(err.code); // SMTP response code, e.g. 550 (numeric)
|
|
645
|
-
console.error(err.command); // failed command, e.g. "RCPT TO"
|
|
646
|
-
}
|
|
647
|
-
if (err instanceof ResendError) {
|
|
648
|
-
console.error(err.statusCode); // HTTP status code
|
|
649
|
-
console.error(err.code); // machine-readable, e.g. "BAD_REQUEST"
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
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`.
|
|
655
|
-
|
|
656
|
-
---
|
|
657
|
-
|
|
658
|
-
## Security
|
|
659
|
-
|
|
660
|
-
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.
|
|
661
|
-
|
|
662
|
-
### Email header & SMTP command injection
|
|
68
|
+
Same shape for every channel — apps use **sently senders**, not vendor SDKs:
|
|
663
69
|
|
|
664
|
-
|
|
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` |
|
|
665
76
|
|
|
666
|
-
|
|
667
|
-
- **Fails closed:** hostile input throws a clear error (with the offending code point) — it is never stripped, repaired, and then accepted.
|
|
668
|
-
- Protects the **display name** too, so an ASCII name like `"Foo\r\nBcc: attacker@evil.com"` can no longer inject a header.
|
|
669
|
-
- Enforced consistently across **From, To, Cc, Bcc, and Reply-To**, and across **every transport** (SMTP, SES, Mailgun, Postmark, Resend, SendGrid, Brevo).
|
|
77
|
+
Full walkthrough: [Get started](https://sently.omqkhafi.dev/docs/get-started).
|
|
670
78
|
|
|
671
|
-
|
|
672
|
-
await mailer.send({
|
|
673
|
-
from: "you@example.com",
|
|
674
|
-
to: { address: "victim@x.com\r\nBcc: attacker@evil.com" },
|
|
675
|
-
subject: "Hi",
|
|
676
|
-
text: "...",
|
|
677
|
-
});
|
|
678
|
-
// → throws: Email address contains a forbidden control character (0x0d)
|
|
679
|
-
```
|
|
79
|
+
## Why sently?
|
|
680
80
|
|
|
681
|
-
|
|
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.
|
|
682
82
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
### Supply chain
|
|
695
|
-
|
|
696
|
-
**Zero runtime dependencies** — there is no transitive dependency tree to audit or to be compromised.
|
|
697
|
-
|
|
698
|
-
---
|
|
699
|
-
|
|
700
|
-
## Bundle size
|
|
701
|
-
|
|
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.
|
|
703
|
-
|
|
704
|
-
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:
|
|
705
|
-
|
|
706
|
-
| How you send | Import | ~gzip |
|
|
707
|
-
|--------------|--------|-------|
|
|
708
|
-
| HTTP API (Resend, SendGrid, …) | `sently/mailer` + `sently/transports/<provider>` | **~6.1 KB** |
|
|
709
|
-
| SMTP relay (`host` / `port`) | `sently/smtp` (or `createSMTPMailer` from `sently`) | **~15 KB** |
|
|
710
|
-
| Transport only (no mailer wrapper) | `sently/transports/<provider>` | **~4.7 KB** |
|
|
711
|
-
|
|
712
|
-
Regenerate full tables with `bun run measure:size:md`. Measured **2026-05-31** (minified + gzip):
|
|
713
|
-
|
|
714
|
-
#### Common stacks
|
|
715
|
-
|
|
716
|
-
| What | Imports | ~gzip |
|
|
717
|
-
|------|---------|-------|
|
|
718
|
-
| HTTP — Resend | `sently/mailer` + `sently/transports/resend` | ~6.1 KB |
|
|
719
|
-
| HTTP — SendGrid | `sently/mailer` + `sently/transports/sendgrid` | ~5.9 KB |
|
|
720
|
-
| HTTP — transport only | `sently/transports/resend` (no `createMailer` wrapper) | ~4.7 KB |
|
|
721
|
-
| SMTP relay | `sently/smtp` with `{ host, port, auth }` | ~14.8 KB |
|
|
722
|
-
| SMTP + Node adapter | `sently/smtp` + `sently/adapters/node` | ~14.8 KB |
|
|
723
|
-
| Main entry + HTTP | `sently` + HTTP transport via main `createMailer` | ~6.1 KB |
|
|
724
|
-
|
|
725
|
-
#### Core entries
|
|
726
|
-
|
|
727
|
-
| What | Imports | ~gzip |
|
|
728
|
-
|------|---------|-------|
|
|
729
|
-
| sently/mailer | Transport-only `createMailer` (plugins, sendBulk) | ~2.6 KB |
|
|
730
|
-
| sently | Main entry — types, factories, OAuth2, `SentlyError` | ~2.6 KB |
|
|
731
|
-
| sently/smtp | SMTP `createSMTPMailer` — host/port, pool, adapters | ~14.7 KB |
|
|
732
|
-
|
|
733
|
-
```ts
|
|
734
|
-
// HTTP
|
|
735
|
-
import { createMailer } from "sently/mailer";
|
|
736
|
-
import { ResendTransport } from "sently/transports/resend";
|
|
737
|
-
|
|
738
|
-
// SMTP
|
|
739
|
-
import { createSMTPMailer } from "sently/smtp";
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
Main `"sently"` exports shared types, `createMailer` (transport), `createSMTPMailer`, `detectRuntime`, OAuth2, and `SentlyError`. Transports, webhooks, idempotency, DKIM, and plugins are separate subpaths only.
|
|
743
|
-
|
|
744
|
-
---
|
|
745
|
-
|
|
746
|
-
## Choosing an entrypoint
|
|
747
|
-
|
|
748
|
-
```
|
|
749
|
-
How do you send mail?
|
|
750
|
-
│
|
|
751
|
-
├─ HTTP API (Resend, SendGrid, …)
|
|
752
|
-
│ import { createMailer } from "sently/mailer"
|
|
753
|
-
│ import { ResendTransport } from "sently/transports/resend"
|
|
754
|
-
│ createMailer({ transport: new ResendTransport({ apiKey }) })
|
|
755
|
-
│
|
|
756
|
-
├─ SMTP relay (host / port / auth)
|
|
757
|
-
│ import { createSMTPMailer } from "sently/smtp"
|
|
758
|
-
│ createSMTPMailer({ host, port, auth })
|
|
759
|
-
│
|
|
760
|
-
└─ Custom / decorated transport (Retry, Idempotency, Preview)
|
|
761
|
-
import { createMailer } from "sently/mailer"
|
|
762
|
-
createMailer({ transport: new RetryTransport(inner) })
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
---
|
|
766
|
-
|
|
767
|
-
## Migrating from Nodemailer
|
|
768
|
-
|
|
769
|
-
| Nodemailer | sently |
|
|
770
|
-
|------------|--------|
|
|
771
|
-
| `nodemailer.createTransport({...})` | `await createSMTPMailer({...})` or `createMailer({ transport })` |
|
|
772
|
-
| `transporter.sendMail(options)` | `mailer.send(options)` |
|
|
773
|
-
| `transporter.verify()` | `mailer.verify()` |
|
|
774
|
-
| `options.attachments[].path` | Same (Node/Bun/Deno); use `content` on edge |
|
|
775
|
-
| `import nodemailer from 'nodemailer'` | `import { createMailer } from 'sently/mailer'` (HTTP) or `createSMTPMailer` from `'sently/smtp'` |
|
|
776
|
-
| CommonJS | ESM only |
|
|
777
|
-
| Node.js only | Node, Bun, Deno, CF Workers |
|
|
778
|
-
|
|
779
|
-
---
|
|
780
|
-
|
|
781
|
-
## TypeScript
|
|
782
|
-
|
|
783
|
-
```typescript
|
|
784
|
-
import type {
|
|
785
|
-
MailOptions,
|
|
786
|
-
MailPlugin,
|
|
787
|
-
SendResult,
|
|
788
|
-
Attachment,
|
|
789
|
-
SMTPConfig,
|
|
790
|
-
SMTPMailerOptions,
|
|
791
|
-
TransportMailerOptions,
|
|
792
|
-
} from "sently";
|
|
793
|
-
```
|
|
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 |
|
|
794
93
|
|
|
795
|
-
|
|
94
|
+
## Entrypoints
|
|
796
95
|
|
|
797
|
-
|
|
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 |
|
|
798
104
|
|
|
799
|
-
##
|
|
105
|
+
## Documentation
|
|
800
106
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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) |
|
|
804
117
|
|
|
805
|
-
|
|
118
|
+
Local docs: `bun run site:dev`. Verify: `bun run verify`.
|
|
806
119
|
|
|
807
|
-
MIT
|
|
120
|
+
Pre-1.0. Published on [npm](https://www.npmjs.com/package/sently) and [JSR](https://jsr.io/@alialnaghmoush/sently). MIT.
|