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.
Files changed (270) hide show
  1. package/AGENTS.md +34 -0
  2. package/CHANGELOG.md +49 -1
  3. package/README.md +89 -776
  4. package/dist/adapters/bun.js +1 -1
  5. package/dist/adapters/cf.js +2 -2
  6. package/dist/adapters/cf.js.map +2 -2
  7. package/dist/adapters/deno.js +1 -1
  8. package/dist/adapters/node.js +1 -1
  9. package/dist/auth/oauth2.js +2 -2
  10. package/dist/auth/oauth2.js.map +1 -1
  11. package/dist/chunk-0gqy32pe.js +4 -0
  12. package/dist/chunk-0gqy32pe.js.map +10 -0
  13. package/dist/{chunk-kebhy657.js → chunk-0qxws3kj.js} +2 -2
  14. package/dist/{chunk-kebhy657.js.map → chunk-0qxws3kj.js.map} +1 -1
  15. package/dist/chunk-29z7fkzy.js +4 -0
  16. package/dist/{chunk-74rha3ac.js.map → chunk-29z7fkzy.js.map} +2 -2
  17. package/dist/chunk-5915vbcj.js +4 -0
  18. package/dist/chunk-5915vbcj.js.map +10 -0
  19. package/dist/{chunk-keksa6fx.js → chunk-6npp3x3c.js} +2 -2
  20. package/dist/{chunk-keksa6fx.js.map → chunk-6npp3x3c.js.map} +1 -1
  21. package/dist/chunk-7gy2q9sh.js +4 -0
  22. package/dist/chunk-7gy2q9sh.js.map +10 -0
  23. package/dist/{chunk-gkkv7yqm.js → chunk-8dty8c4p.js} +3 -3
  24. package/dist/{chunk-gkkv7yqm.js.map → chunk-8dty8c4p.js.map} +2 -2
  25. package/dist/{chunk-qp4y0kp2.js → chunk-8njyga62.js} +2 -2
  26. package/dist/{chunk-qp4y0kp2.js.map → chunk-8njyga62.js.map} +1 -1
  27. package/dist/{chunk-gh5n759p.js → chunk-93vqxxj2.js} +3 -3
  28. package/dist/{chunk-gh5n759p.js.map → chunk-93vqxxj2.js.map} +1 -1
  29. package/dist/chunk-b602dhck.js +4 -0
  30. package/dist/{chunk-rm78s1ek.js.map → chunk-b602dhck.js.map} +2 -2
  31. package/dist/chunk-gp4fwrs8.js +4 -0
  32. package/dist/chunk-gp4fwrs8.js.map +10 -0
  33. package/dist/chunk-n0qeyzqm.js +13 -0
  34. package/dist/{chunk-cvrbfke9.js.map → chunk-n0qeyzqm.js.map} +2 -2
  35. package/dist/chunk-qtkd5bak.js +4 -0
  36. package/dist/chunk-qtkd5bak.js.map +11 -0
  37. package/dist/chunk-skqhj0rm.js +4 -0
  38. package/dist/{chunk-r7ee9tk7.js.map → chunk-skqhj0rm.js.map} +2 -2
  39. package/dist/{chunk-a1c87hn1.js → chunk-tamww15j.js} +2 -2
  40. package/dist/{chunk-a1c87hn1.js.map → chunk-tamww15j.js.map} +1 -1
  41. package/dist/chunk-vjpds2pf.js +3 -0
  42. package/dist/chunk-vjpds2pf.js.map +10 -0
  43. package/dist/chunk-yp311efr.js +4 -0
  44. package/dist/chunk-yp311efr.js.map +10 -0
  45. package/dist/chunk-ywq6s10g.js +5 -0
  46. package/dist/{chunk-d5rwm3br.js.map → chunk-ywq6s10g.js.map} +3 -3
  47. package/dist/core/base64.d.ts +10 -0
  48. package/dist/core/errors.js +2 -2
  49. package/dist/core/errors.js.map +1 -1
  50. package/dist/core/hooks.d.ts +14 -0
  51. package/dist/core/plugin.d.ts +7 -7
  52. package/dist/core/provider-label.d.ts +6 -0
  53. package/dist/core/push-endpoint.d.ts +29 -0
  54. package/dist/core/push-types.d.ts +98 -0
  55. package/dist/core/sms-types.d.ts +76 -0
  56. package/dist/core/smtp.js +2 -2
  57. package/dist/core/smtp.js.map +1 -1
  58. package/dist/core/types.d.ts +27 -4
  59. package/dist/core/whatsapp-types.d.ts +116 -0
  60. package/dist/detect.js +2 -2
  61. package/dist/detect.js.map +1 -1
  62. package/dist/dkim.js +2 -2
  63. package/dist/dkim.js.map +2 -2
  64. package/dist/idempotency.d.ts +1 -0
  65. package/dist/idempotency.js +2 -2
  66. package/dist/idempotency.js.map +3 -3
  67. package/dist/index.d.ts +24 -2
  68. package/dist/index.js +23 -0
  69. package/dist/mailer.js +2 -2
  70. package/dist/mailer.js.map +1 -1
  71. package/dist/observability/console.d.ts +12 -0
  72. package/dist/observability/console.js +3 -0
  73. package/dist/observability/console.js.map +10 -0
  74. package/dist/plugins/react.js +2 -2
  75. package/dist/plugins/react.js.map +2 -2
  76. package/dist/plugins/template.js +1 -1
  77. package/dist/pool/pool.js +2 -2
  78. package/dist/pool/pool.js.map +1 -1
  79. package/dist/push.d.ts +26 -0
  80. package/dist/push.js +3 -0
  81. package/dist/push.js.map +10 -0
  82. package/dist/sms.d.ts +26 -0
  83. package/dist/sms.js +3 -0
  84. package/dist/sms.js.map +10 -0
  85. package/dist/smtp-mailer.js +2 -2
  86. package/dist/smtp-mailer.js.map +2 -2
  87. package/dist/transports/brevo.d.ts +1 -0
  88. package/dist/transports/brevo.js +2 -2
  89. package/dist/transports/brevo.js.map +3 -3
  90. package/dist/transports/cloudflare-email.d.ts +58 -0
  91. package/dist/transports/cloudflare-email.js +3 -0
  92. package/dist/transports/cloudflare-email.js.map +10 -0
  93. package/dist/transports/fallback.d.ts +100 -0
  94. package/dist/transports/fallback.js +3 -0
  95. package/dist/transports/fallback.js.map +9 -0
  96. package/dist/transports/loops.d.ts +32 -0
  97. package/dist/transports/loops.js +3 -0
  98. package/dist/transports/loops.js.map +10 -0
  99. package/dist/transports/mailersend.d.ts +27 -0
  100. package/dist/transports/mailersend.js +3 -0
  101. package/dist/transports/mailersend.js.map +10 -0
  102. package/dist/transports/mailgun.d.ts +1 -0
  103. package/dist/transports/mailgun.js +2 -2
  104. package/dist/transports/mailgun.js.map +3 -3
  105. package/dist/transports/mailtrap.d.ts +36 -0
  106. package/dist/transports/mailtrap.js +3 -0
  107. package/dist/transports/mailtrap.js.map +10 -0
  108. package/dist/transports/msegat.d.ts +132 -0
  109. package/dist/transports/msegat.js +3 -0
  110. package/dist/transports/msegat.js.map +10 -0
  111. package/dist/transports/plunk.d.ts +29 -0
  112. package/dist/transports/plunk.js +3 -0
  113. package/dist/transports/plunk.js.map +10 -0
  114. package/dist/transports/postmark.d.ts +1 -0
  115. package/dist/transports/postmark.js +2 -2
  116. package/dist/transports/postmark.js.map +3 -3
  117. package/dist/transports/preview.d.ts +1 -0
  118. package/dist/transports/preview.js +2 -2
  119. package/dist/transports/preview.js.map +3 -3
  120. package/dist/transports/resend.d.ts +1 -0
  121. package/dist/transports/resend.js +2 -2
  122. package/dist/transports/resend.js.map +3 -3
  123. package/dist/transports/retry.d.ts +1 -0
  124. package/dist/transports/retry.js +2 -2
  125. package/dist/transports/retry.js.map +3 -3
  126. package/dist/transports/sendgrid.d.ts +1 -0
  127. package/dist/transports/sendgrid.js +2 -2
  128. package/dist/transports/sendgrid.js.map +3 -3
  129. package/dist/transports/ses.d.ts +1 -0
  130. package/dist/transports/ses.js +3 -3
  131. package/dist/transports/ses.js.map +3 -3
  132. package/dist/transports/smtp.d.ts +1 -0
  133. package/dist/transports/smtp.js +2 -2
  134. package/dist/transports/smtp.js.map +1 -1
  135. package/dist/transports/sndr.d.ts +41 -0
  136. package/dist/transports/sndr.js +3 -0
  137. package/dist/transports/sndr.js.map +10 -0
  138. package/dist/transports/sparkpost.d.ts +30 -0
  139. package/dist/transports/sparkpost.js +3 -0
  140. package/dist/transports/sparkpost.js.map +10 -0
  141. package/dist/transports/taqnyat-mail.d.ts +33 -0
  142. package/dist/transports/taqnyat-mail.js +3 -0
  143. package/dist/transports/taqnyat-mail.js.map +10 -0
  144. package/dist/transports/taqnyat-phone.d.ts +4 -0
  145. package/dist/transports/taqnyat-sms.d.ts +131 -0
  146. package/dist/transports/taqnyat-sms.js +3 -0
  147. package/dist/transports/taqnyat-sms.js.map +10 -0
  148. package/dist/transports/taqnyat-whatsapp.d.ts +51 -0
  149. package/dist/transports/taqnyat-whatsapp.js +3 -0
  150. package/dist/transports/taqnyat-whatsapp.js.map +10 -0
  151. package/dist/transports/twilio-sms.d.ts +66 -0
  152. package/dist/transports/twilio-sms.js +3 -0
  153. package/dist/transports/twilio-sms.js.map +10 -0
  154. package/dist/transports/webpush.d.ts +46 -0
  155. package/dist/transports/webpush.js +3 -0
  156. package/dist/transports/webpush.js.map +10 -0
  157. package/dist/transports/weighted-fallback.d.ts +65 -0
  158. package/dist/transports/weighted-fallback.js +3 -0
  159. package/dist/transports/weighted-fallback.js.map +10 -0
  160. package/dist/transports/whatsapp-cloud.d.ts +56 -0
  161. package/dist/transports/whatsapp-cloud.js +3 -0
  162. package/dist/transports/whatsapp-cloud.js.map +10 -0
  163. package/dist/webhooks/brevo.js +1 -1
  164. package/dist/webhooks/mailgun.js +1 -1
  165. package/dist/webhooks/postmark.js +1 -1
  166. package/dist/webhooks/resend.js +1 -1
  167. package/dist/webhooks/sendgrid.js +1 -1
  168. package/dist/webhooks/ses.js +1 -1
  169. package/dist/webhooks/sndr.d.ts +16 -0
  170. package/dist/webhooks/sndr.js +3 -0
  171. package/dist/webhooks/sndr.js.map +10 -0
  172. package/dist/webhooks/timing-safe-equal.js +1 -1
  173. package/dist/webhooks.d.ts +9 -2
  174. package/dist/webhooks.js +4 -0
  175. package/dist/whatsapp.d.ts +26 -0
  176. package/dist/whatsapp.js +3 -0
  177. package/dist/whatsapp.js.map +10 -0
  178. package/package.json +121 -4
  179. package/site/README.md +26 -0
  180. package/site/content/docs/ai/index.mdx +13 -0
  181. package/site/content/docs/ai/llms-txt.mdx +12 -0
  182. package/site/content/docs/ai/mcp.mdx +16 -0
  183. package/site/content/docs/ai/meta.json +9 -0
  184. package/site/content/docs/channels/email.mdx +100 -0
  185. package/site/content/docs/channels/hooks.mdx +87 -0
  186. package/site/content/docs/channels/index.mdx +81 -0
  187. package/site/content/docs/channels/meta.json +12 -0
  188. package/site/content/docs/channels/push.mdx +92 -0
  189. package/site/content/docs/channels/sms.mdx +88 -0
  190. package/site/content/docs/channels/whatsapp.mdx +95 -0
  191. package/site/content/docs/decorators/fallback.mdx +34 -0
  192. package/site/content/docs/decorators/idempotency.mdx +34 -0
  193. package/site/content/docs/decorators/index.mdx +50 -0
  194. package/site/content/docs/decorators/meta.json +12 -0
  195. package/site/content/docs/decorators/preview.mdx +34 -0
  196. package/site/content/docs/decorators/retry.mdx +34 -0
  197. package/site/content/docs/decorators/weighted-fallback.mdx +34 -0
  198. package/site/content/docs/get-started/entrypoints.mdx +114 -0
  199. package/site/content/docs/get-started/index.mdx +14 -0
  200. package/site/content/docs/get-started/installation.mdx +78 -0
  201. package/site/content/docs/get-started/introduction.mdx +77 -0
  202. package/site/content/docs/get-started/meta.json +12 -0
  203. package/site/content/docs/get-started/migrate-nodemailer.mdx +23 -0
  204. package/site/content/docs/get-started/runtimes.mdx +17 -0
  205. package/site/content/docs/guides/adapters.mdx +26 -0
  206. package/site/content/docs/guides/attachments.mdx +19 -0
  207. package/site/content/docs/guides/bundle-size.mdx +78 -0
  208. package/site/content/docs/guides/dkim.mdx +26 -0
  209. package/site/content/docs/guides/index.mdx +15 -0
  210. package/site/content/docs/guides/meta.json +21 -0
  211. package/site/content/docs/guides/oauth2.mdx +23 -0
  212. package/site/content/docs/guides/observability.mdx +16 -0
  213. package/site/content/docs/guides/plugins-template.mdx +22 -0
  214. package/site/content/docs/guides/pool.mdx +24 -0
  215. package/site/content/docs/guides/react-email.mdx +20 -0
  216. package/site/content/docs/guides/security.mdx +17 -0
  217. package/site/content/docs/guides/send-bulk.mdx +18 -0
  218. package/site/content/docs/guides/vendor-extras-otp.mdx +18 -0
  219. package/site/content/docs/guides/webhooks.mdx +120 -0
  220. package/site/content/docs/guides/webpush-interop.mdx +18 -0
  221. package/site/content/docs/index.mdx +15 -0
  222. package/site/content/docs/meta.json +15 -0
  223. package/site/content/docs/quick-start/email.mdx +40 -0
  224. package/site/content/docs/quick-start/index.mdx +15 -0
  225. package/site/content/docs/quick-start/meta.json +5 -0
  226. package/site/content/docs/quick-start/push.mdx +36 -0
  227. package/site/content/docs/quick-start/sms.mdx +36 -0
  228. package/site/content/docs/quick-start/whatsapp.mdx +38 -0
  229. package/site/content/docs/reference/errors.mdx +22 -0
  230. package/site/content/docs/reference/exports.mdx +83 -0
  231. package/site/content/docs/reference/index.mdx +15 -0
  232. package/site/content/docs/reference/mail-options.mdx +22 -0
  233. package/site/content/docs/reference/meta.json +15 -0
  234. package/site/content/docs/reference/push-options.mdx +20 -0
  235. package/site/content/docs/reference/sms-options.mdx +17 -0
  236. package/site/content/docs/reference/transport-contracts.mdx +17 -0
  237. package/site/content/docs/reference/webhook-events.mdx +70 -0
  238. package/site/content/docs/reference/whatsapp-options.mdx +15 -0
  239. package/site/content/docs/transports/brevo.mdx +40 -0
  240. package/site/content/docs/transports/cloudflare-email.mdx +40 -0
  241. package/site/content/docs/transports/index.mdx +105 -0
  242. package/site/content/docs/transports/loops.mdx +41 -0
  243. package/site/content/docs/transports/mailersend.mdx +40 -0
  244. package/site/content/docs/transports/mailgun.mdx +42 -0
  245. package/site/content/docs/transports/mailtrap.mdx +42 -0
  246. package/site/content/docs/transports/meta.json +32 -0
  247. package/site/content/docs/transports/msegat.mdx +42 -0
  248. package/site/content/docs/transports/plunk.mdx +40 -0
  249. package/site/content/docs/transports/postmark.mdx +40 -0
  250. package/site/content/docs/transports/resend.mdx +43 -0
  251. package/site/content/docs/transports/sendgrid.mdx +40 -0
  252. package/site/content/docs/transports/ses.mdx +44 -0
  253. package/site/content/docs/transports/smtp.mdx +47 -0
  254. package/site/content/docs/transports/sndr.mdx +144 -0
  255. package/site/content/docs/transports/sparkpost.mdx +41 -0
  256. package/site/content/docs/transports/taqnyat-mail.mdx +41 -0
  257. package/site/content/docs/transports/taqnyat-sms.mdx +41 -0
  258. package/site/content/docs/transports/taqnyat-whatsapp.mdx +40 -0
  259. package/site/content/docs/transports/twilio-sms.mdx +128 -0
  260. package/site/content/docs/transports/webpush.mdx +43 -0
  261. package/site/content/docs/transports/whatsapp-cloud.mdx +42 -0
  262. package/dist/chunk-4mvp3b85.js +0 -3
  263. package/dist/chunk-4mvp3b85.js.map +0 -10
  264. package/dist/chunk-74rha3ac.js +0 -4
  265. package/dist/chunk-cvrbfke9.js +0 -13
  266. package/dist/chunk-d5rwm3br.js +0 -5
  267. package/dist/chunk-ka6t8g77.js +0 -4
  268. package/dist/chunk-ka6t8g77.js.map +0 -11
  269. package/dist/chunk-r7ee9tk7.js +0 -4
  270. package/dist/chunk-rm78s1ek.js +0 -4
package/README.md CHANGED
@@ -1,136 +1,53 @@
1
- # sently
2
-
3
- > Nodemailer is Node.js–only and ships the full mail stack on every import (~58 KB gzip for [v8.0.10](https://bundlephobia.com/package/nodemailer@8.0.10)).
4
- > sently runs on Bun, Deno, and Cloudflare Workers — same familiar API, HTTP stacks from ~6.1 KB via `sently/mailer`.
5
-
6
- ```bash
7
- bun add sently
8
- ```
9
-
10
- [![npm version](https://img.shields.io/npm/v/sently.svg)](https://www.npmjs.com/package/sently)
11
- [![JSR](https://jsr.io/badges/@alialnaghmoush/sently)](https://jsr.io/@alialnaghmoush/sently)
12
- [![bundle size](https://img.shields.io/bundlephobia/minzip/sently)](https://bundlephobia.com/package/sently)
13
- [![license](https://img.shields.io/npm/l/sently.svg)](LICENSE)
14
- [![tests](https://img.shields.io/badge/tests-passing-brightgreen)](#)
15
- [![GitHub](https://img.shields.io/github/stars/alialnaghmoush/sently?style=social&label=GitHub)](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.7.2"`) for production until v1.0.0.
18
-
19
- ---
20
-
21
- ## Why not Nodemailer?
22
-
23
- | Feature | Nodemailer | sently |
24
- |---------|-----------|--------|
25
- | Bundle size | ~58 KB gzip always ([v8.0.10](https://bundlephobia.com/package/nodemailer@8.0.10)) | ~6.1 KB HTTP · ~15 KB SMTP |
26
- | Runtimes | Node.js only | Node, Bun, Deno, CF Workers |
27
- | Module format | CommonJS | ESM only |
28
- | Dependencies | 0 | 0 |
29
- | DKIM signing | ✓ via `nodemailer-dkim` | ✓ built-in (Web Crypto) |
30
- | OAuth2 / XOAUTH2 | ✓ via plugin | ✓ built-in |
31
- | Connection pooling | ✓ | ✓ |
32
- | HTTP transports | ✓ via plugins | ✓ built-in (6 providers) |
33
- | Retry transport | ✗ | ✓ |
34
- | Preview transport | ✗ | ✓ |
35
- | Template engine | ✗ | ✓ |
36
- | `sendBulk()` | ✗ | ✓ (native batch on Resend/SendGrid) |
37
- | React Email | via plugin | ✓ `sently/react` |
38
- | Idempotency keys | ✗ | ✓ `sently/idempotency` |
39
- | Webhook parsing | | `sently/webhooks` |
40
- | TypeScript | via `@types/nodemailer` | ✓ built-in |
41
- | Last release | 2026 (8.0.x) | 2026 |
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
- npm install sently
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
- await mailer.close();
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: "onboarding@yourdomain.com",
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
- template: "welcome",
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
- ### File path (Node.js / Bun / Deno only)
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
- All addresses **and** display names are validated centrally in `parseAddresses()` (and re-asserted when rendering headers), before any normalization:
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
- - Rejects CR, LF, NUL, every other C0 control (`0x00`–`0x1F`), DEL (`0x7F`), and the Unicode line/paragraph separators `U+2028`/`U+2029`.
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
- ```typescript
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
- MIME attachment filenames and custom attachment headers are likewise sanitized against header injection.
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
- ### Credential protection
684
-
685
- - **`requireTLS`** (default `true` when `auth` is set) refuses to authenticate over a cleartext connection, defeating STARTTLS-stripping downgrade attacks.
686
- - **OAuth2 / XOAUTH2** and DKIM signing are built in via Web Crypto — no plaintext secrets in transit beyond what the protocol requires.
687
-
688
- ### Attachments
689
-
690
- > ⚠️ `attachment.path` reads files from disk. Never pass user-controlled paths without validation.
691
-
692
- `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.
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
- All types ship with the package — no separate `@types/` install needed.
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
- ## Links
105
+ ## Documentation
800
106
 
801
- - **Source & issues:** [github.com/alialnaghmoush/sently](https://github.com/alialnaghmoush/sently)
802
- - **npm:** [npmjs.com/package/sently](https://www.npmjs.com/package/sently)
803
- - **JSR:** [jsr.io/@alialnaghmoush/sently](https://jsr.io/@alialnaghmoush/sently)
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
- ## License
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.