RELEASES

Every published sently release, newest first.

v1.2.2

♻️ Changed

  • **Repository owner** — GitHub, docs, and npm Trusted Publisher coordinates moved from alialnaghmoush/sently to omqkhafi/sently
  • **JSR scope** — the published package is now @omqkhafi/sently; @alialnaghmoush/sently stays on the registry but will not receive new versions. README and install docs tell consumers to switch with bunx jsr add @omqkhafi/sently

v1.2.1

✨ Added

  • **Taqnyat Verify OTP live verified** — production sendOtp → handset code → verifyOtp confirmed; docs OTP tab shows the green LiveVerified callout alongside SMS, WhatsApp, and Email

♻️ Changed

  • **Hostinger SMTP unified** — SMTP settings now come from the same name via function overloading: HostingerTransport({ user, pass }) returns a ready SMTPConfig for createSMTPMailer, while new HostingerTransport({ token, mailbox }) stays the Mail API transport; IntelliSense narrows options and the return type by config shape; hostingerSmtpConfig() remains as a 1.x compatibility alias

🐛 Fixed

  • **Taqnyat Verify OTP parsing** — live returnJson: 1 responses put the docs result code in Data.result and use top-level status: 1 as a transport envelope; sendOtp / verifyOtp now read Data.result (and Data.MessageEn / Data.MessageAr) so a successful send is no longer misreported as code 1 (invalid apiKey)

v1.2.0

✨ Added

  • **Hostinger transport** — sently/transports/hostinger sends from a managed Hostinger mailbox through the Hostinger Mail API (Bearer token + mailbox resource ID); listMailboxes() discovers mailbox IDs and verify() checks token scope; vendor extras sendReply / sendForward thread by folder + IMAP UID; hostingerSmtpConfig() fills ready SMTP settings for createSMTPMailer (smtp.hostinger.com, ports 465/587)
  • **Hostinger brand mark** — React HostingerLogo (full wordmark from hostinger.com) / HostingerLogoIcon (purple H); docs title badge uses the wordmark, sidebar/marquee use the H mark
  • **Hostinger docs** — Mail API + SMTP feature tabs (send, HTML, attachments, CC/BCC, reply/forward, mailboxes, verify, SSL/STARTTLS/pool) at the same depth as Taqnyat; links to Hostinger developers / Mail API / SMTP tutorial; green LiveVerified on SMTP (production relay previously proven); homepage marquee marks Hostinger verified
  • **Taqnyat Email live verified** — opt-in live suite delivered a real message via mailSend.php with an approved portal sender; docs show the green LiveVerified callout for Email alongside SMS and WhatsApp

v1.1.1

🐛 Fixed

  • **Publish CI Test job** — build dist before bun test so parallel Verify no longer fails api-surface suites that import published bundles
  • **bun test skips live suites** — clears TAQNYAT_LIVE / SNDR_LIVE / INBUCKET_LIVE and ignores **/*.live.test.ts; bun run test:live is local-only (refuses CI / GITHUB_ACTIONS)

v1.1.0

✨ Added

  • **Web Push live verified** — homepage transport strip marks Web Push verified after okengine delivered a real browser notification with VAPID; docs page shows the green LiveVerified callout
  • **Web Push brand mark** — React WebPushLogo / WebPushLogoIcon (blue notification tile + bell + badge); docs title badge + sidebar/marquee use the mark
  • **Web Push protocol knobs** — urgency / topic headers (RFC 8030), rich Notification fields (badge, image, tag, actions, requireInteraction, renotify), silent / data-only payloads, and generateVapidKeys() on sently/transports/webpush
  • **Inbucket transport** — sently/transports/inbucket for local SMTP capture (defaults localhost:2500 / UI http://localhost:9000) with REST helpers listMailbox, getMessage, getSource, markSeen, deleteMessage, purgeMailbox, and mailboxForAddress; opt-in live suite via INBUCKET_LIVE=1; docs under a new **Email Dev** sidebar group with Mailpit
  • **Mailpit vendor extras** — search, getHeaders, htmlCheck, linkCheck, and setRead on MailpitTransport for local inbox assertions; docs cover the helpers and link to the Mailpit API
  • **Mailpit brand mark** — React MailpitLogo / MailpitLogoIcon from the official SVG; docs title badge + sidebar/marquee use the mark; Mailpit is live-verified in the transport strip
  • **Taqnyat live suite** — opt-in tests/integration/taqnyat.live.test.ts (TAQNYAT_LIVE=1) with free preflight before SMS / Mail / WhatsApp sends; live env keys documented in .env.example
  • **Taqnyat transport docs** — single /docs/transports/taqnyat page under a new **Multi-channel** sidebar group (SMS / WhatsApp / Email sections); green LiveVerified on SMS and WhatsApp only
  • **Taqnyat SMS vendor extras** — getBalance, listSenders, schedule, deleteScheduled on TaqnyatSmsTransport (OTP helpers unchanged)
  • **Taqnyat WhatsApp vendor extras** — listTemplates, createTemplate, deleteTemplate, optIn / optOut, sendWithFailover on TaqnyatWhatsAppTransport
  • **Taqnyat brand marks** — React TaqnyatLogo / TaqnyatLogoIcon replace the TQ monogram; marquee uses the full wordmark alone, sidebar uses a square single-dot icon (docs page keeps the brand wordmark)
  • **Transport strip tiers** — homepage marquee legend: color = live verified, muted = available, sponsors reserved; verified chips use the same icon+label rhythm as the rest (brand-colored mark); full wordmarks stay on docs pages; priority marks sit after a ~25% available lead-in
  • **SNDR brand mark** — React SndrLogo / SndrLogoIcon from [sndr.sh](https://www.sndr.sh/sndr-logo.svg); docs page rewritten with LiveVerified, contact resources, and platform coverage table
  • **SNDR webhooks** — map email.queued, failed, complained, opened, and clicked into normalized EmailEvent types

♻️ Changed

  • **Homepage messaging** — clearer TypeScript notification/messaging positioning (Email · SMS · WhatsApp · Push), one-package signal, provider swap strip (SMTP → SES → Resend), tighter why cards; no section removals
  • **/llms.txt** — agent preamble states notification/messaging library (not a queue); Fumadocs handbook index unchanged underneath
  • **Publish CI** — .github/workflows/publish.yml runs only on v* tag pushes; Verify (typecheck, lint, build, test) and npm / JSR publish jobs run in parallel (no longer sequential npm-then-JSR on push to main)
  • **README** — shieldcn grid/left header with cleaned logo SVGs (no clip-path square), drop Bundlephobia badge, add npm downloads chart; version badges match the okengine pattern (/npm/…, /jsr/…)

🔥 Removed

  • **test.yml workflow** — PR / push CI matrix removed; release Verify lives in publish.yml on version tags

🐛 Fixed

  • **Taqnyat WhatsApp queued accept** — treat statuses: "PENDING" (and template/text accepts without message_id yet) as success instead of an empty id edge case
  • **bun run verify never hits provider live suites** — clears TAQNYAT_LIVE / SNDR_LIVE / INBUCKET_LIVE and ignores **/*.live.test.ts so a leftover shell export cannot send real SMS / email / WhatsApp during verify; use bun run test:live to opt in

v1.0.1

🐛 Fixed

  • **Web Push subject validation** — WebPushTransport rejects invalid VAPID subject values at construction (mailto: / https: only) instead of failing later with an opaque push-service 403

v1.0.0

First stable release. Channel contracts and Supported transports are semver-frozen;

npm publishes ship with provenance attestations and Trusted Publishing (OIDC).

sently is the channel-delivery layer — preference centers, digests, workflows, and

in-app inboxes sit on top (custom or Novu / Knock / Courier), not instead of it.

✨ Added

  • **Stability policy** — what is frozen at 1.x vs allowed without a major (PushOptions union, providerIndex, email-only preview / idempotency)
  • **Support matrix** — Supported vs Available transports and runtimes
  • **Non-goals** — library-not-platform boundaries (no preference centers / digests / workflow builders inside sently)
  • **Security policy** — root SECURITY.md (private reporting, response targets, supported versions for patches, Trusted Publisher setup notes)
  • **Compare guide** — positioning vs vendor SDK piles, Nodemailer, and Novu / Knock / Courier
  • **Failover guide** — cross-channel retry-then-fallback recipes
  • **Mailpit transport** — sently/transports/mailpit for local email capture (SMTP defaults localhost:1025, REST helpers for list / get / delete)

♻️ Changed

  • **Messaging positioning** — homepage, README, llms.txt, and package description/keywords lead with multi-channel scope creep (one sender / error / retry model); bundle size as supporting evidence for edge / Workers; FCM documented as current HTTP API support, not a feature boast
  • **Handbook landing** — install → send loop, channel map, where-next table, and library-vs-platform troubleshooting
  • **Docs IA** — remove duplicate Quick start section; Channels is the first-send path; cross-links for stability, support matrix, failover, and security
  • **Migrate from Nodemailer** — diff-style transporter and sendMailsend swaps, field map, SMTP → HTTP follow-on, fuller options tables
  • **npm publish** — OIDC Trusted Publishing first, optional NPM_TOKEN fallback; continues to use npm publish --provenance from CI (npm CLI ≥11.5.1 check in the publish workflow)
  • **Semver** — public channel factories, contracts, hooks, SentlyError codes, ChannelSendResult, and published subpaths follow 1.x stability

v0.10.0

✨ Added

  • **Channel send result** — toChannelSendResult / ChannelSendResult (sently/channel-result, also on the main barrel) maps email, SMS, WhatsApp, and push results to { messageId, provider, accepted }
  • **Cross-channel decorators** — RetryTransport, FallbackTransport, and WeightedFallbackTransport wrap SMS / WhatsApp / Push transports; SMS / WhatsApp / push senders wire onRetry / onFallback like email
  • **Unifonic SMS** — sently/transports/unifonic (AppSid + el.cloud REST send); webhook parser at sently/webhooks/unifonic
  • **FCM push** — sently/transports/fcm (current FCM HTTP API + service-account JWT, no Google SDK)
  • **Non-email webhooks** — DeliveryEvent plus parsers for Twilio SMS and WhatsApp Cloud (sently/webhooks/twilio-sms, whatsapp-cloud); toDeliveryEvent maps EmailEvent into the shared shape; Twilio and WhatsApp Cloud parsers expose verifySignature for inbound authenticity
  • **Docs landing** — homepage transport marquee and channel cards include Unifonic and FCM

♻️ Changed

  • **PushOptions** — union of WebPushOptions (subscription) and FcmPushOptions (token); Web Push and FCM transports reject the wrong shape
  • **SMS / WhatsApp / Push results** — optional providerIndex when a fallback decorator handles the send

🐛 Fixed

  • **Unifonic success parsing** — accept OpenAPI success: "true" (string) in addition to boolean true
  • **Unifonic statusCallback** — reject non-HTTPS URLs at construction (el.cloud OpenAPI requires a public HTTPS callback)
  • **Empty FCM token** — reject blank token before calling the FCM API
  • **Landing provider strip** — keep transport icons and labels on one vertical midline in the homepage marquee

🔒 Security

  • **WhatsApp Cloud webhooks** — verifySignature for X-Hub-Signature-256 (HMAC-SHA256 over the raw body with the Meta app secret)
  • **Twilio SMS webhooks** — verifySignature for X-Twilio-Signature (HMAC-SHA1 over URL + sorted form params with the Auth Token)

v0.9.2

♻️ Changed

  • **bun run verify** — runs site:build before tests so static-export and docs-site failures block pack/publish
  • **CI Actions** — actions/checkout and actions/setup-node to v7; Node smoke job on Node 24 (drops the Node 24 force-env workaround)

🐛 Fixed

  • **Apple touch icon** — mark apple-icon as force-static so output: "export" can prerender it during site:build

v0.9.1

🐛 Fixed

  • **SNDR live suite opt-in** — require SNDR_LIVE=1 (plus credentials) so bun run verify stays offline-safe when .env has keys
  • **Docs header geometry gate** — assert the home brand cell against HeroIntro's left-pane width after the hero split moved out of page.tsx

v0.9.0

✨ Added

  • **Channel senders** — createSmsSender (sently/sms), createWhatsAppSender (sently/whatsapp), createPushSender (sently/push) alongside email; each sender supports plugins and lifecycle hooks
  • **SMS transports** — Twilio, Taqnyat SMS, Msegat (sently/transports/twilio-sms, taqnyat-sms, msegat); vendor OTP helpers stay on the transport class
  • **WhatsApp transports** — WhatsApp Cloud API, Taqnyat WhatsApp (sently/transports/whatsapp-cloud, taqnyat-whatsapp)
  • **Web Push transport** — VAPID Web Push (sently/transports/webpush)
  • **Email transports** — Sndr, Taqnyat Mail (sently/transports/sndr, taqnyat-mail)
  • **Sndr webhooks** — parseSndrWebhook / verifySndrSignature (sently/webhooks/sndr, also via sently/webhooks)
  • **Per-provider webhook subpaths** — sently/webhooks/{brevo,mailgun,postmark, resend,sendgrid,ses,sndr} for tree-shakeable imports
  • **Docs site** — Next.js + Fumadocs handbook under site/ (bun run site:dev)
  • **Web Push interop example** — examples/webpush-interop
  • **AGENTS.md** — agent contract for the sently-first provider model (replaces CLAUDE.md; published with the package)

♻️ Changed

  • **Library scope** — channel-first messaging (email, SMS, WhatsApp, push), not email-only; package description and README updated accordingly
  • **Docs site branding** — real sently wordmark / mark in the header, homepage, 404, favicon, Apple touch icon, and docs OG images; README shows the logo

v0.8.0

✨ Added

  • **FallbackTransport** (sently/transports/fallback) — provider failover through an ordered transport list; composes with RetryTransport; FallbackError.attempts records { provider, error } per failed attempt
  • **Five HTTP providers:** MailerSend, Plunk, SparkPost, Mailtrap, Loops (sently/transports/mailersend, plunk, sparkpost, mailtrap, loops)
  • **SendResult.provider** and **SendResult.providerIndex** — set by FallbackTransport to identify which provider handled the send
  • **durationMs** optional third argument on onSuccess / onError mailer hooks (backward compatible)
  • **consoleObserver** (sently/observability) — ready-made console logging hooks; also exported from main "sently" barrel
  • **Mailer onFallback hook** — onFallback(ctx, failedProvider, nextProvider, error) on MailerHooks; wired automatically when using FallbackTransport or WeightedFallbackTransport
  • **Provider cooldowns** — FallbackTransport(transports, { cooldownMs }) skips unhealthy providers until cooldown expires; shared across WeightedFallbackTransport sends
  • **verifyAll()** on FallbackTransport / WeightedFallbackTransport — per-provider { ok, providers: [{ provider, ok, message? }] } result; verify() still returns the first healthy provider
  • **WeightedFallbackTransport** (sently/transports/weighted-fallback) — weighted-random primary provider with failover through remaining providers
  • **Transport.provider** — stable provider identifier on every transport class; getProviderLabel() prefers it over constructor.name inference
  • **CloudflareEmailTransport** (sently/transports/cloudflare-email) — Workers send_email binding transport (not fetch HTTP)

v0.7.2

🐛 Fixed

  • Publish workflow runs bun run build before registry steps and publishes **npm before JSR** so a failed npm step does not leave JSR ahead of npm
  • scripts/publish.ts syncVersion() skips rewriting jsr.json when content is unchanged (avoids dirty working tree between CI publish steps)

v0.7.1

📚 Documentation

  • README Nodemailer comparison updated for **v8.0.10** (~58 KB gzip, zero deps, 2026 releases)
  • Bundle size figures refreshed from bun run measure:size:md (~6.1 KB HTTP, ~15 KB SMTP, ~2.6 KB sently/mailer)
  • llms.txt and CLAUDE.md aligned with measured gzip sizes

v0.7.0

💥 Breaking + Migration

  • **Transport-only createMailer** — passing SMTP config (host / port / auth) to createMailer from sently or sently/mailer throws SentlyError (INVALID_CONFIG) at factory time with guidance to use createSMTPMailer.
  • **sently/smtp** — removed createMailer alias; export is **createSMTPMailer only** (also available from main "sently").
  • **CreateMailerOptions** removed — use **TransportMailerOptions** or **SMTPMailerOptions** instead.
  • **Main barrel slimmed** — "sently" no longer re-exports transports, webhook parsers, idempotency, DKIM helpers, template plugin, or SMTPPool. Import from subpaths (e.g. sently/transports/resend, sently/webhooks).
  • **Prior breaks documented in one place:** v0.5.1 react barrel move; v0.6.2 transport-only main createMailer; v0.5.x bulk-send behavior (native batch, default 2 req/s throttle, SendResult.batchError).

✨ Added

  • **INVALID_CONFIG** stable error code for misconfigured mailer factories
  • **SMTPMailerOptions** type for SMTP factory config
  • README decision-tree section and Microsoft 365 OAuth2 example

♻️ Changed

  • **BulkSendOptions.concurrency TSDoc** — documents default 1 (matches implementation)
  • Bundle size budgets updated for slimmer main entry
  • JSDoc and README examples use explicit subpath imports

v0.6.2

✨ Added

  • **sently/smtp** subpath — SMTP createMailer (host / port / auth, pooling, adapters) isolated from the main entry for smaller HTTP app bundles

♻️ Changed

  • **Breaking:** main createMailer from sently is transport-only (same as sently/mailer). SMTP relay apps use import { createMailer } from "sently/smtp" or import { createSMTPMailer } from "sently".
  • **RetryTransport** — auth-failure detection uses SentlyError.sentlyCode instead of importing SMTPError (leaner sently/mailer graph).
  • **mailer.ts** — retry hook wiring duck-types setMailerOnRetry (no static RetryTransport import).
  • **Build** — remove dist/ before each bun run build to avoid stale chunks in published tarballs (~137 KB npm tarball vs ~424 KB with stale chunks).
  • Bundle size budgets and README tables updated for measured gzip sizes (main entry ~2.4 KB; HTTP stack ~6 KB).

v0.6.1

🐛 Fixed

  • **Build** — emit dist/core/errors.js so the main barrel and Node/Deno smoke tests resolve SentlyError imports after bun run build

♻️ Changed

  • Bundle size budgets adjusted for minor measurement drift on CI runners

v0.6.0

✨ Added

  • **sently/errors** — unified SentlyError base class with stable machine-readable codes (RATE_LIMITED, BAD_REQUEST, SMTP_AUTH_FAILED, etc.); all existing transport error classes now extend SentlyError while preserving their names, constructor signatures, and public properties (**additive, backward-compatible**)
  • **Mailer lifecycle hooks** — optional hooks on createMailer (onSend, onSuccess, onError, onRetry) for metrics and tracing on every send; hook context includes to, subject, provider, and messageId only (no body/PII); RetryTransport wires onRetry per attempt (**additive**)
  • **AUDIT-v0.6.md** — README capability audit with file/line proof (all claims verified implemented, including CRAM-MD5)

♻️ Changed

  • Bundle size budgets updated for expanded mailer (hooks) and error hierarchy

v0.5.2

🐛 Fixed

  • **Build** — dist/webhooks.js and dist/react.js are generated from source instead of bundled with code-splitting, fixing SyntaxError: Export 'k' is not defined when importing sently or sently/webhooks in Node.js

v0.5.1

💥 Breaking

  • **Main barrel** — reactPlugin and ReactMailOptions are no longer exported from "sently". Import them from "sently/react" instead.

🐛 Fixed

  • **Webhook signatures** — verifyResendSignature and verifyMailgunSignature now compare decoded signature bytes with constant-time timingSafeEqual (Web Crypto only)
  • **SES webhooks** — SNS Message field accepted when already parsed as an object
  • **Batch sendBulk** — per-chunk failure isolation, per-message partial batch errors (SendResult.batchError), and rate limiting between batch HTTP requests (default 2/s)

♻️ Changed

  • RateLimiter extracted to src/core/rate-limiter.ts (shared by pool and mailer)
  • Transport.batchMax on ResendTransport; BulkSendOptions.rateDelta / rateLimit
  • Bundle size budgets updated for expanded mailer and Resend transport

v0.5.0

✨ Added

  • **sently/react** — React Email integration via reactPlugin(); optional peers react and @react-email/render; options.react on MailOptions
  • **sently/idempotency** — IdempotencyTransport decorator with MemoryIdempotencyStore; dedupe on retry/replay; Resend native Idempotency-Key header support
  • **Native batch in sendBulk** — optional Transport.sendBatch() on Resend (POST /emails/batch, RESEND_BATCH_MAX = 100) and SendGrid (multi- personalization); attachment messages fall back to single-send
  • **stopOnError** option on BulkSendOptions
  • **sently/webhooks** — normalized EmailEvent parsers for Resend, SendGrid, Postmark, Mailgun, SES (SNS envelope + double-encoded Message), and Brevo; optional Mailgun HMAC and Resend Svix signature verification helpers
  • **deduped** flag on SendResult for idempotency cache hits

♻️ Changed

  • sendBulk uses native batch endpoints when the transport implements sendBatch; concurrent per-message fallback unchanged for other transports
  • SendGrid single-send puts subject in personalization (batch-compatible)
  • Bundle size budgets adjusted for expanded sently/mailer and main entry

v0.4.7

✨ Added

  • **sently/mailer entry** — transport-only createMailer without SMTP code in the bundle (~4.3 KB with HTTP transports vs ~14 KB from the main entry)
  • **sently/dkim entry** — optional DKIM signing; MIME lazy-loads it only when dkim config is passed (~1.7 KB)
  • **TransportMailerOptions** type for the sently/mailer entry
  • **Bundle size CI gate** — bun run check:size enforces gzip budgets in tools/bundle-size-budgets.json
  • **bun run measure:size** / **measure:size:md** — categorized bundle reports for docs and CI
  • **Deno and Cloudflare adapter smoke tests** in CI

♻️ Changed

  • Lazy-load SMTP transport and pool from createMailer when using SMTP config
  • Extract MailerImpl to src/mailer.ts; full createMailer in detect.ts delegates to it for custom transports
  • **dist/index.js generated from src/index.ts** via scripts/generate-index-js.ts (no hand-maintained export list)
  • **Minified dist/ output** in production builds
  • **Pinned devDependency versions** (Biome 2.4.16, TypeScript 6.0.3, @types/node 25.9.1, MCP SDK 1.29.0)

📚 Documentation

  • README **Bundle Size** section — import-path guide, common stacks, per-subpath tables, and HTTP stack breakdown
  • Inline JSDoc on src/index.ts barrel re-exports for JSR symbol documentation
  • Corrected bundle size figures (minified + gzip, one decimal KB)

v0.4.6

📚 Documentation

  • Documented all remaining exported symbols (class members, interface properties, and internal helpers) for 100% JSR symbol documentation coverage across adapters, transports, pool, OAuth2, MIME, SigV4, and SMTP protocol types

v0.4.5

📚 Documentation

  • Added @module documentation to the ./pool entrypoint
  • Documented exported interface properties and public API methods across core types, adapters, and transports to meet JSR symbol documentation requirements (80%+ threshold)

v0.4.4

🔒 Security

  • Fixed SigV4 date format: slice(0,15) not slice(0,16) — all real SES requests were producing malformed x-amz-date headers
  • Added requireTLS guard before SMTP AUTH (default: true when auth is set) — prevents credential exposure on STARTTLS-stripping attacks
  • Hardened email address validation against header and SMTP command injection, enforced centrally in parseAddresses() (and re-asserted at render time in toMIMEHeader()):
  • Rejects CR, LF, NUL, all other C0 control characters (0x00–0x1F), DEL (0x7F), and the Unicode line/paragraph separators U+2028/U+2029
  • Fails closed: hostile input throws a clear error with the offending code point instead of being accepted
  • No repair or normalization of malicious input — addresses are never transformed (e.g. CR/LF stripped) and then accepted
  • Protects the display name as well as the address; an ASCII display name such as "Foo\r\nBcc: ..." can no longer inject a header
  • Enforced consistently across every address field (From, To, Cc, Bcc, Reply-To) and every transport (SMTP, SES, Mailgun, Postmark, Resend, SendGrid, Brevo), since all of them route through parseAddresses()
  • Sanitized attachment filenames and custom attachment headers against MIME header injection
  • Fixed basePath startsWith sibling-directory bypass in resolve-attachments (now appends path separator before comparison)
  • Added CRLF guard on EHLO domain for consistency

v0.4.3

✨ Added

  • llms.txt for LLM/agent discovery (install, quick example, subpath exports, and when-to-use guidance)
  • README: Nodemailer comparison table, 30-second tour, error handling, and TypeScript sections
  • CLAUDE.md repository map for agents (core entry, adapters, transports, tests, build)

♻️ Changed

  • README positioning, HTTP transport reference table, plugin docs reorder, and tree-shaking callout
  • package.json description and npm keywords for discoverability

v0.4.2

🐛 Fixed

  • CI: replaced node -e dynamic import with scripts/smoke.mjs (top-level await ESM) for reliable Node.js smoke testing
  • CI: integration test now imports from dist/index.js directly instead of relying on package self-reference resolution
  • CI: updated GitHub Actions to latest patch versions

v0.4.1

🐛 Fixed

  • CI: use locally installed tsc (node_modules/.bin/tsc) in build.ts instead of bunx tsc to avoid runtime npm downloads in CI
  • CI: add bun run build step to SMTP integration job so dist/ exists before the integration test imports from 'sently'
  • CI: improved smoke test error output with explicit .catch() handler
  • CI: added FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 to all workflow jobs

v0.4.0

✨ Added

  • PreviewTransport — writes emails to disk as .eml or HTML for local development
  • RetryTransport — decorator transport with exponential/linear/fixed backoff
  • mailer.sendBulk() — batch send with concurrency control and per-message callbacks
  • templatePlugin + simpleEngine — zero-dependency {{variable}} template rendering
  • verify() on all HTTP transports (Resend, SendGrid, Postmark, Mailgun, SES, Brevo) returns typed VerifyResult instead of boolean
  • MailOptions.template and MailOptions.data fields for template plugin integration
  • SESTransport now accepts dkim config for signing raw MIME messages
  • attachment.path basePath guard (opt-in) in resolveAttachments
  • GitHub Actions CI matrix: unit tests (Bun), smoke test (Node 22), SMTP integration (Mailpit)

🐛 Fixed

  • detectRuntime() priority hardened: Bun checked before Node.js process globals
  • Cloudflare Workers detection uses positive signature (caches + UA), not absence of other runtimes

v0.3.4

🐛 Fixed

  • Stale sendx references in package.json, build.ts, PROGRESS.md
  • JSR badge URL now matches jsr.json scope exactly
  • OAuth2 refresh deduplication: refreshPromise cleared in .finally() to correctly handle rejected refresh attempts
  • SMTPPool.close() now sets draining flag, rejects new sends, and uses Promise.allSettled to drain in-flight messages
  • Audited all buildMIME() call sites — await confirmed present

✨ Added

  • engines field in package.json (Node >= 18, Bun >= 1.0)

v0.3.3

🐛 Fixed

  • Corrected JSR package name in README from @sently/sently to @alialnaghmoush/sently

v0.3.2

🐛 Fixed

  • Biome formatting in MIME header builder (src/core/mime.ts) so bun lint passes

v0.3.1

🔒 Security

  • Fixed CRLF header injection: sanitizeHeaderValue() strips CR/LF from Subject, display names, and custom headers in MIME builder
  • Fixed SMTP command injection: MAIL FROM and RCPT TO throw SMTPError when address contains CR or LF
  • Fixed email address validation: isValidEmail() rejects strings containing CR, LF, or TAB
  • Fixed OAuth2 refresh race condition: concurrent getAccessToken() calls now share a single in-flight refresh Promise
  • Added console.warn when rejectUnauthorized: false is set in Node.js and Bun adapters
  • Added security note in README for attachment.path

v0.3.0

✨ Added

  • Plugin system: plugins array in createMailer() config Plugins are (options: MailOptions) => MailOptions | Promise<MailOptions> functions that run sequentially before message construction
  • MailgunTransport — Mailgun HTTP API (multipart/form-data)
  • SESTransport — AWS SES v2 HTTP API with SigV4 signing (Web Crypto)
  • BrevoTransport — Brevo (formerly Sendinblue) HTTP API
  • TLSOptions.minVersion — set minimum TLS version for legacy SMTP servers

🏁 Parity milestone

    v0.2.0

    ✨ Added

    • DKIM signing (RSA-SHA256 and Ed25519-SHA256) via SMTPConfig.dkim
    • OAuth2 / XOAUTH2 authentication via SMTPAuth.type = 'OAUTH2'
    • Connection pooling via SMTPConfig.pool and SMTPPool
    • Rate limiting via PoolConfig.rateDelta / PoolConfig.rateLimit
    • CRAM-MD5 authentication (pure-JS HMAC-MD5)

    ♻️ Changed

    • npm package name is sently; JSR package name is @sently/sently
    • SMTPAuth.pass is now optional (was required in v0.1)
    • buildMIME() is now async when DKIM config is provided
    • selectAuthMethod priority: XOAUTH2 > CRAM-MD5 > LOGIN > PLAIN
    • createMailer() uses SMTPPool automatically when pool: true

    🐛 Fixed

    • CRAM-MD5 stub now fully implemented

    v0.1.0

    ✨ Added

    • Initial release.