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

mikser-io-post-email

v1.0.1

Published

Email postprocessor for mikser-io — sends rendered output via SMTP and writes .eml audit files. Composes after post-mjml in a chain.

Readme

mikser-io-post-email

Email postprocessor for mikser-io. Reads rendered HTML from a postprocess chain, composes a MIME message, delivers it via SMTP (or any nodemailer transport), and writes the .eml audit file to the output folder. Idempotency falls out of mikser's render manifest — unchanged inputs don't resend.

Sits after post-mjml in the canonical chain, so the same source content ships as a web page and a responsive email built from one MJML layout:

layouts/welcome.html-mjml-email.hbs    # renderer → MJML → post-mjml → HTML → post-email → EML

Install

npm install mikser-io-post-email

Minimal usage

// mikser.config.js
import { documents, layouts, renderHbs, frontMatter } from 'mikser-io'
import { postMjml } from 'mikser-io-post-mjml'
import { postEmail } from 'mikser-io-post-email'

export default {
    plugins: [
        documents(),
        frontMatter(),
        layouts(),
        renderHbs(),
        postMjml(),
        postEmail({
            from: '[email protected]',
            transport: {
                host: 'smtp.example.com',
                port: 587,
                auth: { user: '...', pass: '...' },
            },
        }),
    ],
}
---
to: [email protected]
subject: Welcome, Alice
layout: welcome.html-mjml-email
---

That's the transactional case: one entity, one recipient. The .eml lands at out/welcome.eml and the message ships immediately. On the next build, mikser's render manifest sees unchanged inputs and skips the whole chain — no resend.

Recipient lists — @listname references

For broadcast, define named lists in plugin options and reference them from to/cc/bcc with an @ prefix.

postEmail({
    from: '[email protected]',
    lists: {
        subscribers: async ({ entity }) =>
            (await queryEntities({ type: 'subscriber', meta: { lang: entity.meta.lang } }))
                .map(s => s.meta.email),
        clients:     ['[email protected]', '[email protected]'],
        team:        ['[email protected]'],
        archive:     ['[email protected]'],
    },
    bcc: ['@archive'],     // global audit copy on every send
})
# newsletter
---
to: '@subscribers'
subject: This week
---

# client update + one-off CC
---
to: '@clients'
cc: ['[email protected]']
---

Resolution rule

| Field | Where it comes from | Combine semantics | |---|---|---| | to | entity meta.to only | exclusive — no plugin-options fallback | | cc | entity meta.cc + plugin cc | additive, deduped (case-insensitive) | | bcc | entity meta.bcc + plugin bcc | additive, deduped | | from| entity meta.from ?? plugin from | entity wins; missing entirely → error |

A @listname in any of these fields resolves through options.lists. Spec values can be a literal string, an array, or (ctx) => Promise<string | string[]> where ctx = { entity, runtime, config, lists, logger }.

Errors:

  • to missing → hard error (no silent send-to-nobody).
  • @unknown → hard error naming the missing list.
  • to: [] (empty after resolution) → writes an empty .eml marker, logs debug, no delivery. Valid "no subscribers for this language" case.

Scheduling — sendAt

---
to: '@subscribers'
subject: Friday digest
sendAt: 2026-06-20T09:00:00Z   # ISO 8601, or missing/'now' for immediate
---

Resolution against maxDelay (default '1h'):

| sendAt | What happens | |---|---| | missing / 'now' | Deliver immediately during the chain | | future | Write .eml, queue a row, deliver on drain when due | | recent past (≤ maxDelay) | Catch-up: deliver immediately, warn nothing | | ancient past (> maxDelay) | Write .eml, mark expired_at, log warning, no delivery |

maxDelay is overridable per-entity:

---
sendAt: 2026-06-20T09:00:00Z
maxDelay: 4h                    # tolerate up to 4h of downtime
---

How the queue works

A persistent table (mikser_post_email_queue in runtime/mikser.sqlite) holds future sends.

mikser_post_email_queue (
    id          PRIMARY KEY → mikser_entities(id) ON DELETE CASCADE,
    eml_path,
    send_at, sent_at, expired_at,
    attempts, last_error
)
  • PK on entity id + UPSERT — re-editing sendAt reschedules in place; can't double-queue.
  • FK CASCADE — delete the source doc, queue row vanishes. Schedule follows the file.
  • Drain triggers: on startup, after every cycle (onFinalized), and every 60s in --watch mode.
  • Failures stay queued: attempts and last_error track retries; the next drain re-attempts.
  • Retention: delivered + expired rows are kept for retention (default '90d') then pruned.

Transport

Anything nodemailer.createTransport accepts:

postEmail({
    transport: {
        host: 'smtp.example.com', port: 587, secure: false,
        auth: { user: '...', pass: '...' },
    },
})

With no transport provided, the plugin builds a JSON transport — useful for dev/testing: messages serialize to the .eml on disk but never leave the box.

Dry-run

postEmail({ dryRun: process.env.NODE_ENV !== 'production' })

dryRun: true writes every .eml (so authors can review them in the output folder) but skips transport.sendMail(). The queue still records deliveries (with sent_at) for observability.

Options reference

| Option | Type | Default | | |---|---|---|---| | name | string | 'email' | Chain identifier (used in layout filenames as -email) | | from | string | required (or per-entity from) | Default sender | | lists | Record<string, string \| string[] \| function> | {} | Named recipient lists for @listname references | | cc | spec | none | Global CC, deduped with entity cc | | bcc | spec | none | Global BCC, deduped with entity bcc | | transport | nodemailer config | JSON transport | Delivery target | | maxDelay | duration string | '1h' | How late past sendAt is still acceptable | | retention | duration string | '90d' | How long delivered/expired rows stay in the queue | | dryRun | boolean | false | Write .eml, skip transport |

What it does NOT do (v1)

  • No transport-native scheduling. Setting send_at on SendGrid/Mailchimp/etc. is not exposed — when an entity is rescheduled (frontmatter edited), the transport would hold both the old and new send. The internal queue dedupes via PK; transport-native scheduling can't.
  • No rename-preserving queue. A file rename = old entity DELETE + new entity INSERT; the old queue row cascades out, the new one's queue row inherits the renamed file's sendAt. Acceptable for v1.
  • No throttling. If 10k pending sends drain at once after long downtime, that's transport-side load to manage via SMTP-pool config.
  • No retry policy beyond "next drain tries again". attempts is recorded for visibility; there's no exponential backoff or dead-lettering.

License

MIT