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

@bobfrankston/smtp-direct

v0.1.8

Published

Direct SMTP client — transport-agnostic, no Node.js dependencies, browser-ready

Readme

@bobfrankston/smtp-direct

Direct SMTP client. Transport-agnostic, no Node.js dependencies, browser-ready.

Sibling to @bobfrankston/iflow-direct (IMAP). They share the TCP transport interface (ImapTransport / TransportFactory) but the protocols themselves have nothing in common, so they remain separate packages.

Why

The Node.js SMTP libraries (nodemailer, smtp-connection) hard-depend on node:net/node:tls and won't run in a WebView, browser, or any non-Node host. smtp-direct is a clean-room ~250-line implementation that takes a TCP byte-stream from the caller and speaks SMTP over it. The same code runs unmodified on:

  • Desktop / Node — inject NodeTransport from @bobfrankston/iflow-node.
  • Android (MAUI WebView) / browser — inject BridgeTransport from @bobfrankston/iflow-direct (talks to the native shell's msgapi.tcp).

This means mailx-desktop, mailx-android, the resender server, and any future msgapi.tcp-capable host can share one SMTP code path.

What it implements

  • RFC 5321 — SMTP (EHLO, MAIL, RCPT, DATA, QUIT, RSET)
  • RFC 3207 — STARTTLS upgrade on port 587
  • Implicit TLS on port 465
  • RFC 4954 + 4616 — AUTH PLAIN
  • AUTH LOGIN (legacy, two-step base64 prompt)
  • RFC 4752 — AUTH XOAUTH2 (Google's bearer-token variant; same mechanism Gmail/Outlook use)
  • Multi-line server replies (NNN-... continuation, final NNN ...)
  • Per-recipient RCPT errors (partial accept = success)
  • Dot-stuffing per RFC 5321 §4.5.2
  • SIZE extension hint when the server advertises it

What it doesn't do (by design)

  • MIME assembly. Caller passes a complete RFC 2822 message; smtp-direct only handles the wire protocol.
  • Connection pooling / keep-alive. One client = one connection. For batch sends, reuse the client across sendMail() calls before quit().
  • Retry / queue logic. Caller's outbox owns retry policy.
  • Raw TCP / TLS. Delegated to the injected Transport.

Install

{
    "dependencies": {
        "@bobfrankston/smtp-direct": "file:../path/to/smtp-direct",
        "@bobfrankston/iflow-direct": "file:../path/to/iflow-direct",
        "@bobfrankston/iflow-node": "file:../path/to/iflow-node"   // desktop only
    }
}

Quick start — desktop

import { SmtpClient } from "@bobfrankston/smtp-direct";
import { NodeTransport } from "@bobfrankston/iflow-node";

const smtp = new SmtpClient({
    host: "smtp.gmail.com",
    port: 587,                                  // STARTTLS submission
    auth: { method: "XOAUTH2", user: "[email protected]", token: bearerToken },
    localname: "mybox.example.com",
    verbose: true,
}, () => new NodeTransport());

await smtp.connect();

const result = await smtp.sendMail(
    { from: "[email protected]", to: ["[email protected]", "[email protected]"] },
    rawRfc822Message,
);
console.log(`accepted=${result.accepted.length} rejected=${result.rejected.length}`);

await smtp.quit();

Quick start — Android / WebView

import { SmtpClient } from "@bobfrankston/smtp-direct";
import { BridgeTransport } from "@bobfrankston/iflow-direct";

const smtp = new SmtpClient({
    host: "smtp.example.com", port: 587,
    auth: { method: "PLAIN", user: "me", pass: "secret" },
}, () => new BridgeTransport());

await smtp.connect();
await smtp.sendMail({ from: "[email protected]", to: ["[email protected]"] }, raw);
await smtp.quit();

The WebView host (msger / msga / Android MAUI shell) must expose msgapi.tcpBridgeTransport calls msgapi.tcp.connect, msgapi.tcp.upgradeTLS, etc. All of msger's hosts already do this.

Config

interface SmtpClientConfig {
    host: string;
    port: number;             // 587 (STARTTLS), 465 (implicit TLS), 25 (relay)
    secure?: boolean;         // implicit TLS at connect. Default: port===465
    auth?: SmtpAuth;          // omit for relay / unauthenticated server-to-server
    localname?: string;       // EHLO name. Default: "localhost"
    timeoutMs?: number;       // per-command inactivity. Default: 30000
    verbose?: boolean;        // log SMTP traffic (auth lines redacted)
}

type SmtpAuth =
    | { method: "PLAIN";    user: string; pass: string }
    | { method: "LOGIN";    user: string; pass: string }
    | { method: "XOAUTH2";  user: string; token: string };

sendMail result

interface SmtpSendResult {
    accepted: string[];
    rejected: { address: string; code: number; message: string }[];
    response: string;          // server's final 250 OK after DATA
}

sendMail resolves whenever at least one recipient was accepted. Per-RCPT errors are reported in result.rejected. Only when all recipients are rejected does sendMail throw; the connection is RSET so it can be reused for the next send.

A MAIL FROM rejection or a final DATA rejection always throws.

Reusing a connection

sendMail can be called repeatedly between connect() and quit():

await smtp.connect();
for (const msg of queue) {
    try {
        await smtp.sendMail(msg.envelope, msg.raw);
    } catch (e) {
        console.error(`send failed: ${e}`);
        // connection still usable — RSET was sent on partial-failure path
    }
}
await smtp.quit();

For long sessions, watch the server's idle policy — most servers drop idle SMTP connections after 5 minutes.

Server compatibility tested against

  • Gmail (smtp.gmail.com:587, XOAUTH2) — primary target
  • Office 365 / Outlook (smtp.office365.com:587, XOAUTH2 or PLAIN) — works
  • Generic Dovecot+Postfix (any:587, PLAIN/LOGIN with STARTTLS) — works
  • Implicit TLS submission (any:465) — works

Caveats

  • One command at a time per connection. SMTP is synchronous; pipelining isn't implemented.
  • TLS certificate validation is the transport's job. NodeTransport/BridgeTransport decide whether to verify the server cert.
  • AUTH XOAUTH2 failure surfaces as a generic "AUTH XOAUTH2 failed". The server's base64 error JSON is consumed but not parsed.
  • No SASL mechanism negotiation beyond what the caller specifies. If the server doesn't advertise the chosen AUTH=METHOD, you'll get a 504 from the server rather than a pre-flight error from smtp-direct.

Layout

smtp-direct/
  index.ts          # public exports
  smtp-client.ts    # SmtpClient class, dotStuff helper
  types.ts          # SmtpClientConfig, SmtpAuth, SmtpSendResult, ...
  package.json
  tsconfig.json

License

ISC