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

@laioutr/app-essentials-mailer

v0.1.2

Published

Laioutr Essentials Mailer — the shared mail backbone for essentials apps; v1 handles withdrawal-form submissions.

Readme

@laioutr/app-essentials-mailer

Laioutr npm version License Nuxt

The shared email backbone for the Laioutr essentials-apps family. It provides a reusable SMTP transport, a precompiled email-template renderer, and a sendMail primitive, plus the orchestr action wiring for the flows it ships.

v1 handles withdrawal-form submissions (German Widerruf): it delivers the consumer's withdrawal to the trader (the critical "store notice", retried once on transient connection errors) and sends the consumer a best-effort acknowledgement on a durable medium.

Installation

Add the module to your Laioutr app's nuxt.config:

export default defineNuxtConfig({
  modules: ['@laioutr/app-essentials-mailer'],
})

Configuration

Configuration is stored private-only — it is merged into runtimeConfig['@laioutr/app-essentials-mailer'] and never copied to runtimeConfig.public. SMTP credentials must never reach a client bundle; source them from environment variables in your nuxt.config runtimeConfig block so they stay server-side and can be overridden per-environment:

export default defineNuxtConfig({
  modules: ['@laioutr/app-essentials-mailer'],
  runtimeConfig: {
    '@laioutr/app-essentials-mailer': {
      transport: {
        type: 'smtp',
        host: process.env.SMTP_HOST,
        port: 587,
        secure: false, // true for port 465
        auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
      },
      from: 'Shop <[email protected]>', // sender; may be "Display Name <addr>"
      recipient: '[email protected]', // trader address that receives withdrawal notices
      replyToConsumer: true, // store-notice reply-to = the consumer's email (default true)
      timeZone: 'Europe/Berlin', // optional IANA zone; omitted/invalid values use UTC
      brand: {
        shopName: 'Example Shop', // shown in the email header + copyright line
        shopUrl: 'https://example.com', // optional; header shop name links here
        footerLinks: [
          // optional; rendered in order, ` · `-joined, in the footer
          { label: 'Imprint', url: 'https://example.com/imprint' },
          { label: 'Privacy', url: 'https://example.com/privacy' },
        ],
      },
    },
  },
})

| Key | Type | Notes | | ------------------ | ---------------------------- | ---------------------------------------------------------------------------- | | transport | { type: 'smtp', … } | SMTP connection settings (host, port, secure?, auth). | | from | string | Sender address; "Display Name <addr>" accepted. | | recipient | string | Trader address that receives withdrawal notices. | | replyToConsumer | boolean (default true) | When not false, the store-notice reply-to is the consumer's email. | | timeZone | string (optional) | IANA timezone for timestamps and copyright year; omitted/invalid values use UTC. | | brand | { shopName, shopUrl?, footerLinks? } | Shop branding for every email's header + footer (required). | | brand.shopName | string | Shop display name — header and © {year} {shopName} footer line. | | brand.shopUrl | string (optional) | Storefront URL the header shop name links to; plain text when omitted. | | brand.footerLinks| { label, url }[] (optional)| Footer links (imprint, privacy, …), rendered in order; omitted when empty. |

timeZone affects the local calendar date/time shown in every email and the copyright year at New Year boundaries. The footer displays the effective canonical IANA identifier so the timestamp remains unambiguous. Omitted or invalid identifiers fall back to UTC.

There is no config validation by design — a misconfiguration surfaces as a failed send at request time, not at boot.

Server API

Reusable, server-only primitives are exported from the /server subpath (no #imports, no Nuxt globals — safe to import from any Nitro server context):

import {
  sendMail,
  resolveTransport,
  renderFormEmail,
  createSmtpTransport,
} from '@laioutr/app-essentials-mailer/server'
  • sendMail(transport, message, options?) — send one fully-addressed message with retry + per-attempt timeout policy ({ retries?, timeout? }).
  • resolveTransport(config) / createSmtpTransport(smtp) — the transport seam.
  • renderFormEmail(opts) — fill the compiled form-email layout with per-request data → { html, text }.

Sending mail from a server handler

Inside this module (orchestr actions, server routes, Nitro handlers) use the auto-imported useMailer composable rather than wiring the transport by hand. It reads the private config once, resolves the transport, and hands back a bound sender plus the render context:

import { useMailer } from '#imports' // auto-imported in server context; the import is optional

// `locale` and `vars` come from the request (e.g. clientEnv.locale + the form input).
const { sendMail, ctx, config } = useMailer(locale)

await sendMail(
  {
    ...renderWithdrawalStoreNotice(ctx, vars), // a template → { subject, html, text }
    from: config.from,
    to: config.recipient,
    replyTo: vars.email,
  },
  { retries: 1 }, // SendOptions: retries (default 0) + per-attempt timeout (default 10s)
)
  • useMailer(locale){ sendMail, ctx, config }.
  • sendMail(message, options?) — the delivery primitive bound to the config's transport, so no transport argument here (unlike the /server export).
  • ctx — the MailRenderContext (locale, timezone, brand) every template takes as its first argument.
  • config — the resolved MailerConfig, for addressing (from, recipient, replyToConsumer).

A template maps (ctx, vars){ subject, html, text }; spread that over the addressing fields (from, to, optional replyTo) to form the MailMessage that sendMail delivers.

How emails are authored

Templates live in emails/*.vue and are authored with Maizzle (Vue-SFC email framework: Outlook-safe HTML + CSS inlining). They are precompiled at build time (pnpm build:emails) into HTML layouts under src/runtime/emails/compiled/ — Maizzle is a devDependency and never ships in a consumer's runtime bundle.

Dynamic values are not evaluated by Vue. Each token is wrapped in v-pre so it survives compilation literally, and at runtime the layout is filled with Handlebars — a small runtime template engine. This means templates express real runtime logic ({{#each}} loops, {{#if}} conditionals), not just fixed placeholders, while keeping the heavy authoring pipeline out of production. Handlebars' default {{ }} escaping HTML-escapes all interpolated data, so user-supplied values can never inject markup. Plaintext is derived from the rendered HTML via html-to-text.

Extensibility (future)

The mailer is built to become the shared email backbone for the essentials-apps family. Multi-app template contribution is not implemented in v1, but the seams exist:

  • Exportable Maizzle config (maizzle.config.mjs) — the single styling/pipeline source.
  • Typed layout renderers — each compiled layout has its own typed renderer (renderFormEmail for the form-email layout); a new layout adds a new renderer, and new messages add new templates over it.
  • Runtime template engine (Handlebars) — contributed templates can use runtime {{#each}}/{{#if}}, not only fixed placeholders.
  • Configurable compile globs (scripts/build-emails.mjs templateDirs list) — v1 lists only this app's emails/.
  • Shared server API (@laioutr/app-essentials-mailer/server) — sendMail, resolveTransport, renderFormEmail.

When a second essentials app needs to send email, add a registration hook so apps contribute a raw-template directory (+ optional components) that is compiled at the consumer build against the shared config into Nitro server assets, then rendered/sent through the shared API. At that point @maizzle/framework moves from a devDependency to a build-time dependency (present at the consumer build, still tree-shaken from the runtime bundle). This mirrors how Laioutr apps already contribute orchestrDirs/sections/blocks.

Development

Create a .npmrc from .npmrc.config and fill in NPM_LAIOUTR_TOKEN (needed for @laioutr-* installs), then:

pnpm install
pnpm dev:prepare   # build:emails + module stub + prepare playground
pnpm test          # build:emails + vitest
pnpm lint

pnpm build:emails regenerates the compiled layouts from emails/*.vue — rerun it after editing a template.

License

MIT