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

@opentf/esrun-smtp

v0.2.2

Published

SMTP client for esrun: STARTTLS and implicit TLS, PLAIN/LOGIN/XOAUTH2, MIME messages, pooling.

Readme

@opentf/esrun-smtp

An SMTP client for esrun, written entirely in JavaScript over runtime:net. There is no native code in this package, and none was added to the runtime for it.

npm install @opentf/esrun-smtp
import { env } from "runtime:process";
import { createTransport } from "@opentf/esrun-smtp";

const mail = createTransport({
  host: "smtp.example.com",
  user: "[email protected]",
  password: env.SMTP_PASSWORD,
});

const sent = await mail.send({
  from: "Example App <[email protected]>",
  to: "[email protected]",
  subject: "Welcome",
  text: "Hello, Ada.",
  html: "<p>Hello, <b>Ada</b>.</p>",
});
// { messageId: "<…@example.com>", accepted: ["[email protected]"], rejected: [], response: "2.0.0 queued as …" }

Run it with the network grant scoped to the server:

esrun --allow-net=smtp.example.com --allow-env=SMTP_PASSWORD app.js

Connecting

| Option | | | --- | --- | | host | The server. | | port | Defaults by security: 465, 587, or 25. | | security | "tls" — TLS from the first byte (port 465). "starttls" — plaintext, upgraded with STARTTLS, which the server must offer. "none" — plaintext. Defaults to "tls" on port 465 and "starttls" otherwise. | | user, password | A PLAIN or LOGIN login. | | user, accessToken | An XOAUTH2 login (Gmail, Microsoft 365). Obtaining and refreshing the token is the OAuth provider's business. | | ca | Extra trust anchors (PEM), for a server whose certificate a private authority signed. | | servername | The name the certificate must carry, when it is not host. | | allowPlaintextAuth | Permits a login over security: "none". Off by default. | | name | What EHLO announces. Defaults to localhost. | | timeout | Milliseconds per reply. Defaults to 60 000. | | dataTimeout | Milliseconds for the reply to the end of a message. Defaults to 600 000. | | maxConnections | Sessions open at once. Defaults to 2. | | maxMessages | Messages per session before it is replaced. Defaults to 100. | | idleTimeout | Milliseconds an unused session is kept. Defaults to 30 000. | | dkim | Signs every message — raw ones included — with DKIM. { domain, selector, privateKey }, or a list of them for one signature each. See DKIM. |

A URL works too: createTransport("smtps://user:[email protected]"), with smtp: for STARTTLS on 587 and ?security=none for plaintext.

Secure by default

  • Certificates are always verified. There is no option to turn that off; a private authority is added with ca.
  • STARTTLS is required, not attempted. A server that does not offer it is an error rather than a silent downgrade to plaintext.
  • A login over plaintext is refused unless allowPlaintextAuth is set — for a local relay or a test server. Nothing is sent before it is refused.
  • A header value containing a line break is refused, not stripped: silently repairing an injection attempt would hide it.

verify() opens a session — connect, TLS, login — and returns it to the pool, which checks the settings at startup rather than at the first email.

Messages

| Field | | | --- | --- | | from | "[email protected]", "Ada <[email protected]>", or { name, address }. | | to, cc, bcc, replyTo | One address or a list. bcc goes on the envelope only, never in a header. | | subject | Any Unicode; encoded as RFC 2047 words when not ASCII. | | text, html | Either or both. Both makes multipart/alternative. | | attachments | { filename, content, contentType?, cid? }. content is a string, Uint8Array, ArrayBuffer, or anything with arrayBuffer() — a Blob, or runtime:fs's file(). With cid, the attachment is inline, for HTML that shows it as <img src="cid:…">. | | headers | Further headers by name. The ones the fields build are refused here. | | messageId, date | Default to <uuid@sender's domain> and now. | | envelope | { from, to }, overriding the addresses the server is told. |

The message is always 7-bit — quoted-printable text, base64 attachments, encoded-word headers — so it is deliverable through any relay. Domains are sent in their ASCII form (bücher.example → xn--bcher-kva.example). An address with a non-ASCII local part needs the server's SMTPUTF8, and is refused without it.

sendRaw(envelope, message) sends a message built elsewhere, as it is. Its lines are still normalised to CRLF, dot-stuffed, and checked against SMTP's 998-octet limit.

Results and errors

send() resolves once the server has accepted the message for at least one recipient: accepted lists who, and rejected who not, each with the server's reply. If every recipient is refused, it throws.

Errors are SmtpErrors with a code from SmtpErrorCode, permanent — true when trying again will not help (a 5xx reply, a refused login), false when it may (a 4xx, a lost connection, a timeout) — and the server's reply:

import { SmtpError } from "@opentf/esrun-smtp";

try {
  await mail.send(message);
} catch (e) {
  if (e instanceof SmtpError && !e.permanent) queue.retryLater(message);
  else throw e;
}

| Code | | | --- | --- | | ERR_SMTP_CONNECTION | Could not connect, or the connection was lost. | | ERR_SMTP_TLS | TLS failed, or the certificate did not verify. | | ERR_SMTP_AUTH | The login was refused, or none offered fits the credentials. | | ERR_SMTP_PLAINTEXT_AUTH | A login over plaintext without allowPlaintextAuth. | | ERR_SMTP_SENDER | MAIL FROM refused. | | ERR_SMTP_RECIPIENTS | Every RCPT TO refused; e.rejected says how. | | ERR_SMTP_MESSAGE | The message refused after DATA. | | ERR_SMTP_TOO_LARGE | Larger than the server's SIZE; nothing was sent. | | ERR_SMTP_UNSUPPORTED | Needs STARTTLS, SMTPUTF8 or 8BITMIME, which the server lacks. | | ERR_SMTP_INVALID_MESSAGE | A header with a line break, no recipient, a line over 998 octets. | | ERR_SMTP_DKIM | A DKIM key that cannot sign, a domain or selector that is not a DNS name, or a message without From. Nothing was sent. | | ERR_SMTP_TIMEOUT | No reply within timeout. | | ERR_SMTP_PROTOCOL | The server's reply was not SMTP. | | ERR_SMTP_CLOSED | The transport was closed. |

Sessions

Sessions are pooled: a TLS handshake and a login are most of what one message costs. Up to maxConnections are open at once, each carries up to maxMessages, and an unused one is closed after idleTimeout. The idle timer does not keep the process alive. A pooled session the server has since dropped is replaced and the message retried on the new one, once. close() ends them all.

With the server's PIPELINING, MAIL FROM, every RCPT TO and DATA go in one write — one round trip for the lot, however many recipients.

DKIM

| Option | | | --- | --- | | domain | The signing domain (d=). For DMARC it must be the From address's domain or a parent of it. An internationalised domain goes in its xn-- form. | | selector | The selector (s=): receivers fetch the public key from <selector>._domainkey.<domain>. | | privateKey | A PEM — PRIVATE KEY (PKCS#8, RSA or Ed25519) or RSA PRIVATE KEY (PKCS#1) — or a CryptoKey for RSASSA-PKCS1-v1_5 with SHA-256, or Ed25519. An RSA key must be at least 1024 bits; generate 2048. An encrypted PEM is refused. |

The algorithm follows the key: rsa-sha256 or ed25519-sha256 (RFC 8463). Canonicalization is relaxed/relaxed. The signature covers the whole body and, when present, From, Sender, Reply-To, To, Cc, Subject, Date, Message-ID, In-Reply-To, References, MIME-Version, Content-Type, Content-Transfer-Encoding, List-Unsubscribe and List-Unsubscribe-Post, each listed once more than it occurs, so a header added in transit cannot change what the reader sees. The key is imported on the first send, or by verify().

Not yet

Delivery status notifications (DSN) and CHUNKING.