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

@birtalanrobert/comms

v1.1.0

Published

Outbound email and SMS behind provider ports, and inbound email parsing

Downloads

81

Readme

@birtalanrobert/comms

Outbound email and SMS behind provider ports, inbound email parsing, and the log of both.

Deliberately partial. Templates per locale and tone, quiet hours, SMS segment counting and the metered credit ledger are later work; writing them now would be guessing at requirements three projects away. What is here is the seam, and the half of it that had to come first.

Using it in a NestJS application

import { CommsModule } from '@birtalanrobert/comms/nestjs';
import { NoopMessagePort } from '@birtalanrobert/comms';

@Module({
  imports: [
    // …config, logger, database…
    CommsModule.forRootAsync({
      inject: [ConfigModule.token()],
      useFactory: (config: AppConfig) => ({
        ports: { email: new NoopMessagePort('email') }, // a real provider from Phase 5
        inbound: { domain: config.INBOUND_DOMAIN, secret: config.INBOUND_SECRET },
      }),
    }),
  ],
})
export class AppModule {}

@Global(), and it provides CommsService. Register commsEntities and commsMigrations with the database module.

Both options are optional: a deployment that does not accept inbound mail omits inbound and inboundAddressFor returns undefined; a channel with no port records a failed message rather than throwing, so a missing provider shows up in the log instead of as a 500.

Inbound addresses

A per-request address a client can forward an existing document to — the feature that lets someone send a bank statement they already have in their inbox without opening anything.

const address = comms.inboundAddressFor(`request-${id}`);
// [email protected]

The address is the credential. Anyone who knows it can attach a file to a request, so it carries an HMAC tag: without one, a predictable local part means a stranger can post documents into a firm's workflow and the firm cannot tell.

Two details that are easy to get wrong:

  • A separate secret from the one signing links. An address is printed in email clients, forwarded, and quoted in replies for years; a link expires in days. Sharing a secret between something long-lived and public and something short-lived and private means rotating either breaks the other.
  • The tag is hex, not base64url. Local parts are case-sensitive on paper and lowercased by providers in practice, so parse normalises case — which a base64url tag would not survive, and every address minted would fail to verify itself.

find picks ours out of a forwarded message's recipients, because a client forwards to us and copies their accountant.

Parsing

parseMime reads an RFC 5322 message far enough to be useful: headers with folding, RFC 2047 encoded words, multipart trees, base64 and quoted-printable, legacy charsets, and attachments with their bytes intact.

Written rather than depended on, for one reason: this runs on every inbound message, and inbound mail is the most hostile input the system accepts — anyone can send it, from anywhere, in any shape. Eighty lines that can be read in full and tested against the cases that actually arrive is a smaller permanent attack surface than a general-purpose parser that knows every corner of MIME in order to be asked about six.

What it does not do is left undone rather than half-done. An unrecognised part becomes an attachment with the bytes intact, which is a failure a person can act on.

InboundParser is the port. Every candidate provider offers either parsed JSON in its own shape or the raw message, so an adapter is a function into InboundMessage and choosing differently later is a new adapter rather than a change to anything that consumes mail.

The message log

Two jobs, and the second shapes the table.

The first is answering "did my client actually get that reminder?", which a professional asks whenever a deadline passes quietly. Without a log the honest answer is "we think so".

The second is not doing the same thing twice. Providers redeliver inbound webhooks — that is how at-least-once delivery works — and without a record of what has been handled, a client's forwarded bank statement is attached to their request three times. A partial unique index on the provider's id is what makes the handler idempotent, and it is a database constraint rather than a check in code because two redeliveries can arrive at the same moment.

A message that cannot be routed is logged as discarded rather than dropped. Someone will eventually ask why a forwarded document never appeared, and "it went to an address nobody issued" is an answer only a log can give.

The body is never stored. A reminder is innocuous; inbound mail here is bank statements, and a log table is the last place they should be sitting when someone asks for an erasure.

Sending

MessagePort per channel. What it exposes is fixed by behaviour rather than by any one vendor's API: the sender identity, because alphanumeric sender IDs are permitted in some markets and not others and that decides whether a client can reply; and the segment count, because that is what a credit ledger is debited by.

send records accepted, not delivered — the provider has taken it, and whether it reached a person is a later webhook's news, recorded by settle. A receipt for a message this system has no record of is ignored rather than inserted: it belongs to another environment sharing the provider account.

NoopMessagePort accepts everything and sends nothing. Unlike the file scanner's default, permissive is right here: not sending an email is a visible nuisance, while not scanning a file is invisible and dangerous.

Attachments

await comms.send({
  channel: 'email',
  to: '[email protected]',
  subject: 'Documents from Ion Popescu',
  text: 'Everything they sent is attached.',
  attachments: [{ filename: 'Ion_Popescu.zip', content: archive, contentType: 'application/zip' }],
});

Bounded by MAX_ATTACHMENT_BYTES (10 MB) and refused before the port sees it. Providers differ — many refuse at 10 MB, most at 25 — and base64 inflates an attachment by a third, so the useful limit sits under the smallest of them. Refusing here turns a silent late bounce into a log entry with a sentence in it.

The log records how many files and how many bytes, never their names: it is read by support, and a client's filenames are not theirs to read.