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

@upyo/core

v0.6.0

Published

Simple email sending library for Node.js, Deno, Bun, and edge functions

Readme

@upyo/core

JSR npm

Core types and interfaces for Upyo, a cross-runtime email library that provides a unified, type-safe API for sending emails across Node.js, Deno, Bun, and edge functions.

The @upyo/core package provides the foundational types and interfaces that all Upyo transport implementations use. It defines the common Message, Address, Transport, and Receipt types that enable seamless switching between different email providers while maintaining consistent type safety and error handling.

Features

  • Universal types: Common interfaces for all email transports
  • Type-safe messaging: Comprehensive TypeScript definitions for email messages
  • Attachment support: File attachment handling
  • Cross-runtime compatibility: Works on Node.js, Deno, Bun, and edge functions
  • Zero dependencies: Lightweight with no external dependencies

Installation

npm  add     @upyo/core
pnpm add     @upyo/core
yarn add     @upyo/core
deno add jsr:@upyo/core
bun  add     @upyo/core

Usage

Creating messages

The createMessage() function provides a convenient way to create email messages:

import { createMessage } from "@upyo/core";

const message = createMessage({
  from: "[email protected]",
  to: ["[email protected]", "[email protected]"],
  cc: "[email protected]",
  subject: "Hello from Upyo!",
  content: {
    text: "This is a plain text message.",
    html: "<p>This is an <strong>HTML</strong> message.</p>",
  },
  priority: "high",
});

Since Upyo 0.6.0, internationalized mailbox addresses are accepted, including UTF-8 local parts and Unicode domains:

const message = createMessage({
  from: "josé@example.com",
  to: "用户@例子.广告",
  subject: "Hello",
  content: { text: "Welcome!" },
});

Support for delivering these addresses depends on the selected transport. The @upyo/smtp transport negotiates RFC 6531 SMTPUTF8 automatically.

Adding attachments

Attachments can be added using the standard File API:

import { createMessage } from "@upyo/core";

const message = createMessage({
  from: "[email protected]",
  to: "[email protected]",
  subject: "Document attached",
  content: { text: "Please find the document attached." },
  attachments: [
    new File(
      [await fetch("document.pdf").then(r => r.arrayBuffer())],
      "document.pdf",
      { type: "application/pdf" }
    ),
  ],
});

Handling receipts

All transport operations return Receipt objects that use discriminated unions for type-safe error handling:

import type { Receipt } from "@upyo/core";

function handleReceipt(receipt: Receipt) {
  if (receipt.successful) {
    console.log("Message sent with ID:", receipt.messageId);
  } else {
    console.error("Send failed:", receipt.errorMessages.join(", "));
    console.error("Retryable:", receipt.retryable ?? false);

    for (const error of receipt.errors ?? []) {
      console.error(error.category, error.code, error.provider);
    }
  }
}

Failed receipts keep the legacy errorMessages array and can also carry structured errors for programmatic handling. Transports use categories such as auth, rate-limit, network, timeout, validation, rejected, server-error, service-unavailable, configuration, and unknown.

When implementing a transport, use createFailedReceipt() to keep these fields consistent:

import { createFailedReceipt } from "@upyo/core";

const receipt = createFailedReceipt("HTTP 429: Too Many Requests", {
  provider: "example",
  statusCode: 429,
  retryAfterMilliseconds: 30_000,
});

Implementing custom transports

The Transport interface defines the contract for all email providers:

import type { Message, Receipt, Transport, TransportOptions } from "@upyo/core";

class MyCustomTransport implements Transport<"example"> {
  readonly id = "example";

  async send(
    message: Message,
    options?: TransportOptions,
  ): Promise<Receipt<"example">> {
    // Implementation details...
    return { successful: true, messageId: "12345", provider: this.id };
  }

  async *sendMany(
    messages: Iterable<Message> | AsyncIterable<Message>,
    options?: TransportOptions,
  ): AsyncIterable<Receipt<"example">> {
    for await (const message of messages) {
      yield await this.send(message, options);
    }
  }
}

Related packages

The @upyo/core package is the foundation for all Upyo transport implementations:

| Package | JSR | npm | Description | | ------------------- | ------------------------------ | ------------------------------ | ------------------------------------------------- | | @upyo/smtp | JSR | npm | SMTP transport for any mail server | | @upyo/mailgun | JSR | npm | Mailgun HTTP API transport | | @upyo/sendgrid | JSR | npm | SendGrid HTTP API transport | | @upyo/ses | JSR | npm | Amazon SES HTTP API transport | | @upyo/mock | JSR | npm | Mock transport for testing | | @upyo/opentelemetry | JSR | npm | OpenTelemetry observability for Upyo transports |

Documentation

For comprehensive documentation, examples, and guides, visit https://upyo.org/.

API reference documentation is available on JSR: https://jsr.io/@upyo/core.

Raw MIME delivery

This feature is introduced in Upyo 0.6.0.

RawTransport extends Transport with an optional sendRaw() capability. Use isRawTransport() before sending through a transport supplied by another component; decorators must explicitly expose this capability themselves.

import { isRawTransport, type Transport } from "@upyo/core";

declare const transport: Transport;
if (isRawTransport(transport)) {
  await transport.sendRaw({
    envelope: { from: "[email protected]", to: ["[email protected]"] },
    content: new TextEncoder().encode("Subject: Hello\r\n\r\nHello!\r\n"),
    encoding: "7bit",
  });
}

The envelope is required and independent of all MIME headers, including Bcc. Use null for a null reverse-path. Content accepts Uint8Array, a Promise of bytes, Blob, or an attachment-style factory that opens an independent reader on every call. Do not pass a one-shot stream directly.

An explicit encoding reads the source once per successful send. Omitting it reads twice: analysis followed by transmission. Factories must reproduce identical bytes on both passes, and on concurrent sends. Upyo checks structure and known size again but does not compare a digest of the two passes.

7bit requires ASCII bytes. 8bit allows non-ASCII body bytes and asserts that all MIME headers, including nested part headers, are ASCII. This is a caller guarantee: Upyo checks only the top-level headers and does not parse nested MIME. Use utf8 or omit encoding if you cannot guarantee ASCII headers throughout. utf8 permits internationalized headers. Automatic analysis conservatively selects utf8 for any non-ASCII byte, even in the body; specify 8bit to avoid that additional transport requirement when the headers are ASCII. These values do not request transcoding and do not permit binary MIME with NUL bytes.

Raw content must use CRLF throughout, end in CRLF, have a nonempty header section, and contain no NUL or line longer than 998 bytes excluding CRLF. Headers without a body are valid. Upyo validates these wire constraints, not full MIME syntax or signatures, and never repairs the content. Server-side processing can still modify the message.

Transport implementers can use the raw-message source helpers to validate and read incrementally. Extra memory is limited to bounded work buffers, the largest chunk supplied by the source, and runtime buffers. Sources should honor cancellation promptly and release resources when iteration ends.