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

@kilnjs/mailer

v0.1.3

Published

Outbound mail for KilnJS: one transport, configured where the module is declared, and recorded rather than sent in a test run

Readme

@kilnjs/mailer

Outbound mail for KilnJS, as a module core is given rather than one it contains. SMTP, over nodemailer.

One transport, because SMTP reaches every relay worth reaching and which one is a host rather than a decision. What is a decision is whether mail leaves the process at all, and a test run says no.

Install

yarn add @kilnjs/mailer
import { mailer } from "@kilnjs/mailer";

export default {
  modules: [mailer()],
} satisfies AppConfig;

Declaring it reads the configuration, so a missing host or sender stops the boot rather than the first message.

import { Mail } from "@kilnjs/mailer";

await Mail.send({
  to: user.email,
  subject: "You have been invited",
  html: render(invite),
});

What a message says, and who should get one, stays in the application. This is the part that talks to a relay.

Settings

| | | | | --- | --- | --- | | from | MAIL_FROM | required. A message with no sender is refused by every relay worth using | | host | SMTP_HOST | required | | port | SMTP_PORT | 587 | | user | SMTP_USER | omitted means no authentication, which is what a local relay wants | | password | SMTP_PASSWORD | | | secure | SMTP_SECURE | false. TLS from the first byte, which is port 465; 587 upgrades with STARTTLS on its own | | timeout | SMTP_TIMEOUT_MS | 10000. How long the relay has to connect, to greet and to answer on the socket. nodemailer's own defaults for those are two minutes, thirty seconds and ten minutes | | driver | MAIL_DRIVER | smtp, and memory in a test run | | pool | SMTP_POOL | false. Connections held open between messages | | maxConnections | SMTP_MAX_CONNECTIONS | 5, with a pool | | maxMessages | SMTP_MAX_MESSAGES | 100 per connection, with a pool | | rejectUnauthorized | SMTP_REJECT_UNAUTHORIZED | true. Whether the relay's certificate has to verify | | name | SMTP_CLIENT_NAME | what this server calls itself in EHLO | | replyTo | | on every message that does not name one | | headers | | under whatever a message declares |

Passed to mailer() or read from the environment, field at a time, so the sender can be declared and the password left where secrets live.

driver: "memory" is a deployment that captures its mail rather than sending it: every message goes to listSentMail() and no relay is reached. That is what a test run already does, and MAIL_DRIVER is how a staging box gets it without a code change.

pool is for a relay that charges for connections or rate limits them; without one each message opens and closes its own, which is what a local relay and most hosted ones want. rejectUnauthorized: false is for an internal relay with its own certificate authority and nothing else, which is why it is a line somebody has to write.

A test run sends nothing

Every message is recorded instead, and listSentMail() is what a suite asserts against:

await Mail.send({ to: "[email protected]", subject: "Welcome", text: "..." });

expect(listSentMail()[0]?.subject).toBe("Welcome");

No host is needed for that, and none is reached. A suite cannot write to a real address however it is configured, which is the same guarantee @kilnjs/storage gives for files.

A reply has to thread

An answer to a message a contact sent needs the three headers their client threads on, or it lands as a new thread and their reply to it cannot be matched back by Message-ID:

await Mail.send({
  to: contact.email,
  subject: `Re: ${conversation.subject}`,
  text: body,

  messageId: `<${reply.id}@${ENV.INBOUND_DOMAIN}>`,
  inReplyTo: incoming.messageId,
  references: [...conversation.messageIds, incoming.messageId],
});

headers carries anything else a sender needs. All of it reaches the relay and is recorded by listSentMail(), so a suite asserts on threading without driving nodemailer itself. Before this the type carried to, subject, text and html and little else, so an application that had to thread bypassed this module altogether.

readMailerSettings() is exported for the same reason: it is the one that reads the environment, so something reusing the relay settings does not have to rebuild them from options it does not have.

Awaiting it

Mail.send is awaited, so a handler that cannot continue without the mail having gone can say so. A message nobody is waiting on belongs on a queue: put it on one with @kilnjs/jobs and let the worker send it.

Mail.verify() asks the relay whether it answers and accepts the credentials, for a health check. The connection pool is closed on shutdown.

A refusal carries the relay's own code. The error raised when a relay will not take a message has responseCode in its context, with relayCode (EENVELOPE, ESOCKET, ETIMEDOUT) and relayCommand beside it, so a sender can tell a 450 worth trying again from a 550 that never will be. Without them a bounce and a blip were the same error.

Development

yarn verify   # ts:check, lint, build, exports:check, test

The tests reach no relay: what they cover is how the settings are read and that a test run records rather than sends.

What a transport refuses lives on the contract rather than in the SMTP transport, because a test run is served by the memory one: a rule only the real transport applied would be a rule no suite ever checks, and the first thing to apply it would be the relay in production.