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

@techtas/mailkit

v1.1.0

Published

Zero-config nodemailer wrapper. Test mode uses Resend SMTP, production accepts any SMTP provider.

Readme

@techtas/mailkit

Zero-config nodemailer wrapper with environment-aware SMTP setup. Built by TechtaCloud.

npm version npm downloads license

Never set up nodemailer boilerplate again. One install, one config object, done.


Table of Contents


Installation

npm install @techtas/mailkit nodemailer

nodemailer is a peer dependency — you install it alongside the package.


Quick Start

Test mode (Resend SMTP)

const { createMailer } = require("@techtas/mailkit");

const mailer = createMailer({
  env: "test",
  resendApiKey: process.env.RESEND_API_KEY,
  from: "[email protected]",
});

await mailer.send({
  to: "[email protected]",
  subject: "Hello from MailKit",
  html: "<p>It works!</p>",
});

Production mode (any SMTP provider)

const { createMailer } = require("@techtas/mailkit");

const mailer = createMailer({
  env: "production",
  host: process.env.SMTP_HOST,
  port: 587,
  secure: false,
  username: process.env.SMTP_USER,
  password: process.env.SMTP_PASS,
  from: "[email protected]",
});

await mailer.send({
  to: "[email protected]",
  subject: "Welcome!",
  html: "<h1>Welcome aboard</h1>",
});

Recommended pattern — environment-driven singleton

// lib/mailer.js
const { createMailer } = require("@techtas/mailkit");

const mailer = createMailer(
  process.env.NODE_ENV === "production"
    ? {
        env: "production",
        host: process.env.SMTP_HOST,
        port: Number(process.env.SMTP_PORT) || 587,
        secure: process.env.SMTP_SECURE === "true",
        username: process.env.SMTP_USER,
        password: process.env.SMTP_PASS,
        from: process.env.MAIL_FROM,
      }
    : {
        env: "test",
        resendApiKey: process.env.RESEND_API_KEY,
        from: process.env.MAIL_FROM,
      }
);

module.exports = mailer;

Import it anywhere in your app:

const mailer = require("./lib/mailer");

await mailer.send({ to: "...", subject: "...", html: "..." });

API Reference

createMailer(config) / new MailKit(config)

Creates a mailer instance. Accepts a single config object.

Test mode config:

| Field | Type | Required | Description | |---|---|---|---| | env | "test" | ✅ | Enables Resend SMTP mode | | resendApiKey | string | ✅ | Your Resend API key | | from | string | ✅ | Sender address (must be verified in Resend) |

Production mode config:

| Field | Type | Required | Description | |---|---|---|---| | env | "production" | ✅ | Enables custom SMTP mode | | host | string | ✅ | SMTP host | | port | number | | Default: 587 | | secure | boolean | | Default: auto (true if port 465) | | username | string | ✅ | SMTP username | | password | string | ✅ | SMTP password | | from | string | ✅ | Default sender address | | pool | boolean | | Enable connection pooling. Default: false |


mailer.send(payload)

Send a single email. Returns a Promise<SendMailResult>.

const result = await mailer.send({
  to: "[email protected]",          // string or string[]
  subject: "Hello",
  html: "<p>Hello</p>",            // html or text required
  text: "Hello",
  from: "[email protected]", // optional: overrides default from
  cc: "[email protected]",
  bcc: "[email protected]",
  replyTo: "[email protected]",
  attachments: [],                 // nodemailer attachments array
});

// result: { success: true, messageId: "<[email protected]>" }
// result: { success: false, error: "..." }

mailer.sendBulk(payloads[])

Send multiple emails sequentially. Returns Promise<SendMailResult[]> in the same order as input.

const results = await mailer.sendBulk([
  { to: "[email protected]", subject: "Hi A", text: "Hey" },
  { to: "[email protected]", subject: "Hi B", text: "Hey" },
  { to: "[email protected]", subject: "Hi C", html: "<b>Hey</b>" },
]);

Runs sequentially by design to avoid SMTP rate-limit errors.


mailer.verify()

Verify SMTP connection. Call on app startup to catch misconfiguration before the first send.

const ok = await mailer.verify();
if (!ok) {
  console.error("Mail server unreachable");
  process.exit(1);
}

mailer.environment

Returns the current environment: "test" or "production".

console.log(mailer.environment); // "test"

mailer.close()

Close pooled SMTP connections. Call during graceful shutdown when pool: true.

process.on("SIGTERM", () => {
  mailer.close();
  process.exit(0);
});

Environment Variables

# Test mode
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
[email protected]

# Production mode
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=apikey
SMTP_PASS=SG.xxxxxxxxxxxxxxxxxxxx
[email protected]

Roadmap

v1.0.0 — Core Mailer (done ✅)

Released. The foundation — environment-aware nodemailer wrapper with Resend SMTP for test mode and full custom SMTP for production.

What's included:

  • createMailer(config) factory + MailKit class
  • send() — single email with full header support (cc, bcc, replyTo, attachments)
  • sendBulk() — sequential multi-send with per-result status
  • verify() — startup connection check
  • pool support for high-volume production use
  • JSDoc types for IntelliSense without TypeScript

Install:

npm install @techtas/mailkit nodemailer

v1.1.0 — Named Providers + Retry Logic

Status: planned

Right now production mode requires you to know the SMTP host, port, and secure settings for your provider. v1.1.0 removes that friction with named provider adapters — and adds automatic retry so transient failures don't silently drop emails.

Named provider adapters

Pass a provider name and just your API key. MailKit resolves the SMTP config internally.

// Before (v1.0.0)
createMailer({
  env: "production",
  host: "smtp.sendgrid.net",
  port: 587,
  secure: false,
  username: "apikey",
  password: process.env.SENDGRID_API_KEY,
  from: "[email protected]",
});

// After (v1.1.0)
createMailer({
  env: "production",
  provider: "sendgrid",
  apiKey: process.env.SENDGRID_API_KEY,
  from: "[email protected]",
});

Supported providers at launch:

| Provider | provider value | |---|---| | Resend | "resend" | | SendGrid | "sendgrid" | | Postmark | "postmark" | | AWS SES | "ses" | | Mailgun | "mailgun" | | Custom SMTP | "smtp" (existing behavior) |

Retry logic

Transient SMTP failures (timeouts, connection resets) are common. v1.1.0 adds automatic retry with exponential backoff, configurable per-mailer.

createMailer({
  env: "production",
  provider: "sendgrid",
  apiKey: process.env.SENDGRID_API_KEY,
  from: "[email protected]",
  retry: {
    attempts: 3,      // total attempts (including first)
    delay: 1000,      // base delay in ms (doubles each retry)
  },
});

Retry behavior:

  • Attempt 1 → fails → wait 1000ms
  • Attempt 2 → fails → wait 2000ms
  • Attempt 3 → fails → returns { success: false, error: "..." }

Only network/transport errors trigger retry. Invalid recipients, auth failures, and validation errors fail immediately.

SendMailResult in v1.1.0:

{
  success: false,
  error: "Connection timeout",
  attempts: 3  // how many attempts were made
}

Development guide for v1.1.0

Provider map structure (src/providers.js):

const PROVIDERS = {
  resend: {
    host: "smtp.resend.com",
    port: 465,
    secure: true,
    authUser: "resend",        // fixed username
    authPassField: "apiKey",   // which config field holds the password
  },
  sendgrid: {
    host: "smtp.sendgrid.net",
    port: 587,
    secure: false,
    authUser: "apikey",
    authPassField: "apiKey",
  },
  postmark: {
    host: "smtp.postmarkapp.com",
    port: 587,
    secure: false,
    authUser: null,            // null = use apiKey as both user and pass
    authPassField: "apiKey",
  },
  // ...
};

Retry wrapper (src/retry.js):

async function withRetry(fn, { attempts = 1, delay = 1000 } = {}) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1) throw err;
      await sleep(delay * Math.pow(2, i));
    }
  }
}

Breaking changes: none. Existing host/port/username/password config still works. provider is additive.


v1.2.0 — HTML Template Engine

Status: planned

Instead of building HTML strings manually in every project, v1.2.0 lets you register named templates and pass variables. MailKit handles rendering.

Usage

// Register templates once (e.g. in lib/mailer.js)
mailer.registerTemplate("welcome", {
  subject: "Welcome to {{appName}}, {{name}}!",
  html: `
    <h1>Welcome, {{name}}!</h1>
    <p>Click below to verify your email.</p>
    <a href="{{verifyLink}}">Verify Email</a>
  `,
});

mailer.registerTemplate("otp", {
  subject: "Your OTP code",
  html: `<p>Your code is <strong>{{code}}</strong>. Expires in {{expiresIn}}.</p>`,
});

// Send using template name
await mailer.send({
  to: "[email protected]",
  template: "welcome",
  data: {
    appName: "MyApp",
    name: "Peter",
    verifyLink: "https://myapp.com/verify?token=abc123",
  },
});

Built-in templates

MailKit ships with ready-to-use base templates you can use immediately or override:

| Template name | Use case | |---|---| | mailkit:welcome | New user welcome + email verify link | | mailkit:otp | One-time password / verification code | | mailkit:reset-password | Password reset link | | mailkit:invoice | Basic invoice with line items | | mailkit:notification | Generic notification with title + body |

await mailer.send({
  to: "[email protected]",
  template: "mailkit:otp",
  data: { code: "482910", expiresIn: "10 minutes" },
});

File-based templates

For more complex layouts, load templates from .html files:

mailer.loadTemplatesFrom("./emails"); // loads all .html files in the folder
emails/
  welcome.html
  invoice.html
  otp.html

Each file can use {{variable}} mustache-style interpolation.

Development guide for v1.2.0

Template registry (src/templates.js):

class TemplateRegistry {
  constructor() {
    this._templates = new Map();
    this._loadBuiltins();
  }

  register(name, { subject, html, text }) {
    this._templates.set(name, { subject, html, text });
  }

  render(name, data = {}) {
    const tpl = this._templates.get(name);
    if (!tpl) throw new Error(`[MailKit] Template "${name}" not found.`);
    return {
      subject: interpolate(tpl.subject, data),
      html: tpl.html ? interpolate(tpl.html, data) : undefined,
      text: tpl.text ? interpolate(tpl.text, data) : undefined,
    };
  }
}

function interpolate(str, data) {
  return str.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] ?? "");
}

send() change — detect template field and resolve before sending:

async send(payload) {
  if (payload.template) {
    const rendered = this._templates.render(payload.template, payload.data);
    payload = { ...payload, ...rendered };
  }
  // existing send logic...
}

Breaking changes: none. html/text direct sends still work unchanged.


v2.0.0 — Webhook Tracking

Status: planned

Track email opens, link clicks, bounces, and unsubscribes. v2.0.0 ships in two tiers:

  • Provider-native tracking — delegate to Resend/SendGrid's built-in tracking via their APIs (fast to ship, provider-specific)
  • Self-hosted tracking — MailKit injects a tracking pixel and wraps links itself (provider-agnostic, requires a small HTTP server)

Provider-native tracking (Resend)

// Get events for a sent email
const events = await mailer.getEvents(messageId);

// events:
// [
//   { type: "delivered", timestamp: "2024-01-01T10:00:00Z" },
//   { type: "opened",    timestamp: "2024-01-01T10:05:00Z" },
//   { type: "clicked",   url: "https://myapp.com", timestamp: "..." },
// ]

Webhook listener (self-hosted tracking)

// Express integration
const { createTrackingRouter } = require("@techtas/mailkit/tracking");

app.use("/mail/track", createTrackingRouter({
  secret: process.env.MAILKIT_WEBHOOK_SECRET,
  onOpen: ({ messageId, timestamp, ip }) => {
    console.log("Email opened:", messageId);
  },
  onClick: ({ messageId, url, timestamp }) => {
    console.log("Link clicked:", url);
  },
  onBounce: ({ messageId, reason }) => {
    console.log("Bounced:", reason);
  },
  onUnsubscribe: ({ email }) => {
    // remove from your list
  },
}));

Link tracking

When tracking is enabled, MailKit wraps all links in sent HTML emails through a redirect URL that logs the click before forwarding:

createMailer({
  env: "production",
  provider: "sendgrid",
  apiKey: process.env.SENDGRID_API_KEY,
  from: "[email protected]",
  tracking: {
    enabled: true,
    baseUrl: "https://yourapp.com/mail/track", // your tracking endpoint
    secret: process.env.MAILKIT_WEBHOOK_SECRET,
  },
});

Original link: https://myapp.com/dashboard Wrapped link: https://yourapp.com/mail/track/click?mid=abc&url=aHR0cHM6...&sig=xxx

Open tracking

A 1×1 transparent pixel is injected at the bottom of every HTML email:

<img src="https://yourapp.com/mail/track/open?mid=abc&sig=xxx" width="1" height="1" />

Development guide for v2.0.0

Tracking module structure:

src/
  tracking/
    index.js        ← createTrackingRouter() export
    pixel.js        ← inject open-tracking pixel into HTML
    links.js        ← wrap links in HTML with redirect URLs
    hmac.js         ← sign/verify tracking URLs to prevent spoofing
    providers/
      resend.js     ← Resend Events API adapter
      sendgrid.js   ← SendGrid Activity API adapter

HMAC signing (prevents fake open/click requests):

const crypto = require("crypto");

function signTrackingUrl(params, secret) {
  const payload = new URLSearchParams(params).toString();
  return crypto.createHmac("sha256", secret).update(payload).digest("hex");
}

function verifyTrackingUrl(params, sig, secret) {
  const expected = signTrackingUrl(params, secret);
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Pixel injection (src/tracking/pixel.js):

function injectPixel(html, trackingUrl) {
  const pixel = `<img src="${trackingUrl}" width="1" height="1" style="display:none" />`;
  if (html.includes("</body>")) {
    return html.replace("</body>", `${pixel}</body>`);
  }
  return html + pixel;
}

Breaking changes in v2.0.0:

  • send() gains an optional tracking override (per-send disable)
  • SendMailResult gains optional trackingId field
  • Tracking router is a separate entry point (@techtas/mailkit/tracking) — no bundle size impact if unused

Contributing

Pull requests are welcome. For major changes, open an issue first to discuss.

git clone https://github.com/PetersGlory/techta-mailkit
cd techta-mailkit
npm install

Run the smoke test:

npm test

License

MIT — built by TechtaCloud