npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@radonsdk/email

v0.1.0

Published

One unified, provider-agnostic API for 16 email providers — Western, African, and raw cloud. Write email.send() once, swap providers via config. Batch sends, template interpolation, and normalized delivery webhooks. Own your data, no vendor lock-in.

Readme

@radonsdk/email

One unified, provider-agnostic API for 16 email providers — Western, African, and raw cloud. Write email.send() once and swap providers with a config change, never a code change.

Radon Email gives you a single, typed interface (send, sendBatch, templates, webhooks.handle, …) that every provider implements. Provider-specific quirks — auth schemes, wildly different request bodies, signed webhook formats — are absorbed inside each adapter and never leak to your code. Own your data, no vendor lock-in.

  • 16 providers, one API — Resend, SendGrid, Postmark, AWS SES, Mailgun, Brevo, Loops, Termii (Africa-first), Mailjet, SparkPost, Elastic Email, MailerSend, SMTP2GO, generic SMTP, Amazon Pinpoint, Zoho ZeptoMail.
  • Normalized delivery webhooks — every provider's opens / clicks / bounces / complaints / unsubscribes collapse into one event schema; one handler, email.webhooks.handle(req).
  • Batch sending with graceful sequential fallback, {{variable}} templates, and a bring-your-own-provider interface.
  • Lazy-loaded adapters — a Resend + SendGrid app never bundles SES's code. The core is ~40 KB.
  • Strict TypeScript, ESM + CJS, Node ≥ 18, zero required dependencies (SES/Pinpoint sign with hand-rolled SigV4; only the generic SMTP fallback needs nodemailer, an optional peer dep).

Install

npm install @radonsdk/email
# generic SMTP fallback only:
npm install nodemailer

Radon never stores your secrets. Each adapter reads credentials from RADON_<PROVIDER>_* environment variables (or from the providers config block, which takes precedence). See .env.example for every provider's variable names.


Quickstart (< 5 minutes)

import { RadonEmail } from "@radonsdk/email";

const email = new RadonEmail({
  providers: { resend: {} },                 // creds come from RADON_RESEND_API_KEY
  defaultFrom: "Acme <[email protected]>",
});

const result = await email.send({
  to: "[email protected]",
  subject: "Welcome, {{name}}!",
  html: "<p>Hi {{name}}, thanks for joining.</p>",
  variables: { name: "Ada" },                // free {{var}} interpolation
});

console.log(result.status); // "queued" | "sent" | "scheduled"
console.log(result.id);     // provider message id

Switch providers without touching your send code — it's a config change:

const email = new RadonEmail({
  providers: { resend: {}, termii: {} },
  licenseKey: process.env.RADON_LICENSE_KEY, // termii is a Pro provider
  defaultFrom: "Acme <[email protected]>",
});
await email.init();

await email.send({ to: "[email protected]", subject: "Hi", text: "…" }, { provider: "resend" });
await email.send({ to: "[email protected]", providerOptions: { templateId: "…" } }, { provider: "termii" });

Free vs. Pro

Radon Email gates on two levels — matching @radonsdk/auth and @radonsdk/payments.

1. Provider tier. Three providers are free; the rest require a Radon Pro license key, verified once in await email.init() and cached for the process lifetime.

| Tier | Providers | | --- | --- | | Free | resend, sendgrid, smtp | | Pro (license) | postmark, ses, mailgun, brevo, loops, termii, mailjet, sparkpost, elasticemail, mailersend, smtp2go, pinpoint, zeptomail + any bring-your-own provider |

2. Feature tier. sendBatch(), template management (email.templates.*), and webhook normalization (email.webhooks.handle()) are Pro regardless of provider — they require a verified license even on a free provider like Resend. A single send() (including free {{var}} interpolation) never does.

const email = new RadonEmail({
  providers: { resend: {} },                  // a FREE provider…
  licenseKey: process.env.RADON_LICENSE_KEY,   // …but Pro features still need a license
});
await email.init();                            // verifies the license; unlocks Pro
await email.sendBatch([ /* … */ ]);            // Pro feature
  • Free provider + single send() works with no license and no init().
  • Using a Pro provider or a Pro feature without a valid license throws LicenseRequiredError; a bad/unreachable key throws LicenseInvalidError (fail-closed).
  • Get a license at https://radonsdk.xyz/pricing. Introspect tiers with the exported FREE_PROVIDERS set and isProProvider(slug).

The unified API

// Send one (returns a normalized SendResult).
const r = await email.send({ to: "[email protected]", subject: "Hi", html: "<p>Hi</p>" });

// Batch (Pro). Uses a native bulk endpoint where the provider has one, else
// falls back to sequential sends — `result.batched` tells you which.
const batch = await email.sendBatch([
  { to: "[email protected]", subject: "1", text: "…" },
  { to: "[email protected]", subject: "2", text: "…" },
]);

// Managed templates (Pro to manage; interpolated at send time).
await email.templates.register({ id: "welcome", subject: "Hi {{name}}", html: "<p>{{name}}</p>" });
await email.send({ to: "[email protected]", template: { id: "welcome", variables: { name: "Ada" } } });

Every message supports to/cc/bcc (one or many), replyTo, html/text, attachments, tags, headers, metadata, and sendAt. Operations a provider can't do throw a typed UnsupportedOperationError — never a silent failure — and provider.capabilities tells you up front what's available.

Attachments

await email.send({
  to: "[email protected]",
  subject: "Your invoice",
  text: "Attached.",
  attachments: [
    { filename: "invoice.pdf", content: pdfBuffer },              // Buffer
    { filename: "logo.png", content: base64Png, disposition: "inline", contentId: "logo" },
  ],
});

Templates

  • Free — inline interpolation. Any subject/html/text may contain {{variable}} placeholders; pass variables and Radon interpolates them provider-agnostically, before the adapter runs. Dotted paths ({{user.name}}) work; unknown variables render empty.
  • Pro — managed templates. Register named templates in the email.templates store and send with template: { id, variables }.
  • Native provider templates (SendGrid dynamic templates, Mailgun/Postmark stored templates, …) are exposed via the escape hatch, not reinvented:
// SendGrid dynamic template — passed straight through to the provider.
await email.send({
  to: "[email protected]",
  providerOptions: { templateId: "d-abc123", dynamicTemplateData: { name: "Ada" } },
}, { provider: "sendgrid" });

Webhooks — one handler for every provider (Pro)

Each provider signs (or doesn't sign) delivery webhooks differently — Svix, ECDSA, SNS-RSA, HMAC-SHA256/512. Radon verifies the signature inside the adapter and hands you one normalized event array:

// Express / Fastify / Next — pass the RAW body (signatures are computed over exact bytes).
app.post("/webhooks/email", async (req, res) => {
  try {
    const events = await email.webhooks.handle(
      { body: req.rawBody, headers: req.headers },
      { provider: "resend" },           // omit when only one provider is configured
    );
    for (const e of events) {
      switch (e.type) {
        case "delivered": /* … */ break;
        case "bounced":   /* e.bounce?.type === "hard" | "soft" */ break;
        case "complained":
        case "unsubscribed":
        case "opened":
        case "clicked": break;
      }
    }
    res.sendStatus(200);
  } catch {
    res.sendStatus(400);                // signature failed → reject
  }
});

Normalized event types: delivered, opened, clicked, bounced, complained, unsubscribed, dropped, deferred, failed, unknown. You can also react with hooks: email.on("onBounced", …), email.on("onComplained", …), email.on("onEmailEvent", …).

| Signed & verified | Normalized, unsigned* | No delivery webhook | | --- | --- | --- | | Resend, SendGrid, SES, Mailgun, MailerSend | Postmark, Brevo, Mailjet, SparkPost | Termii, Loops, Elastic Email, SMTP2GO, Pinpoint, ZeptoMail, SMTP |

*These providers don't sign their webhook bodies — Radon still normalizes them, but secure the endpoint yourself (secret path / basic auth). Introspect with provider.capabilities.webhookSignature.


Termii — first-class, not an afterthought

Termii is a leading African (Nigeria-first) messaging platform, and a core reason Radon isn't just another Western-only SDK. It's built to Termii's real API, which is transactional-template / OTP driven (no arbitrary-body send), and keyed by an email_configuration_id from your Termii dashboard:

// Template email
await email.send({
  to: "[email protected]",
  subject: "Your balance",
  variables: { name: "Ada", balance: "₦5,000" },
  providerOptions: { templateId: "your-termii-template-id" },
}, { provider: "termii" });

// OTP-code email
await email.send({
  to: "[email protected]",
  providerOptions: { code: "123456" },
}, { provider: "termii" });

A Termii send with neither a template id nor an OTP code throws a clear SendError rather than silently doing nothing — the same "no silent stubs" honesty the whole suite follows.


Escape hatch

Anything the unified API doesn't cover:

  • message.providerOptions — merged into the outgoing request by the adapter (e.g. a native template id, a provider-specific tracking flag).
  • result.raw — the untouched provider response.
  • email.native(slug) — the provider's own configured HTTP client / context, for calling endpoints Radon doesn't model.

Bring your own provider

Implement the EmailProvider interface (or extend BaseProvider) and register it:

import { registerProvider, BaseProvider } from "@radonsdk/email";

class MyEspProvider extends BaseProvider {
  readonly name = "my-esp";
  readonly capabilities = { send: true, batch: false, templates: false, attachments: true,
    webhooks: false, webhookSignature: false, tags: false, scheduling: false, cc: true, bcc: true, replyTo: true };
  async send(message) { /* … call your API via this.http(...) … */ }
}

registerProvider("my-esp", async () => MyEspProvider);

Custom providers are Pro-gated (like any non-free slug).


Install size & lazy loading

The core (@radonsdk/email) contains zero provider code — only the interface, the registry, the template/webhook/hook engines, the license gate, and shared types. Adapters are loaded on demand via dynamic import() and are also importable directly:

import { ResendProvider } from "@radonsdk/email/providers/resend";

A splitting bundler only includes the adapters you actually use.


License

MIT © Radon SDK. The Radon Pro tier is governed by the license key you configure; see https://radonsdk.xyz/pricing. "# radonsdk-email"