@jagu.cz/email-layer

A small Nuxt layer for sending e-mail with one type-safe entry point — `sendEmail({ … })` — across pluggable **providers** (Zimbra, SMTP, or your own), reusable **contacts**, and reusable **templates**.
2,013
Install
npm install @jagu.cz/email-layer
Latest Version:3.0.1
Last Updated:Jul 10, 2026
Links: npm
Maintainer: v-moravec

Nuxt Email Layer

A small Nuxt layer for sending e-mail with one type-safe entry point — sendEmail({ … }) — across pluggable providers (Zimbra, SMTP, or your own), reusable contacts, and reusable templates.

pnpm add @jagu.cz/email-layer

Add it to your nuxt.config.ts:

export default defineNuxtConfig({
  extends: ['@jagu.cz/email-layer'],
})

Auto-imports & where things live

The public API — sendEmail, defineEmailContact, defineEmailTemplate, defineEmailProvider, registerEmailProvider — is auto-imported in the Nitro server context. Don't import it; just call it. It is server-only (Nitro routes, plugins, server utils) — it is not available in Vue components or anywhere on the client.

Where each piece belongs in your project:

File Holds
server/utils/emails.ts Your CONTACTS / TEMPLATES definitions (auto-imported)
server/api/*.ts Route handlers that call sendEmail(...)
server/plugins/*.ts registerEmailProvider(...) for custom providers
nuxt.config.ts + env Provider selection & credentials

Quick start

export default defineEventHandler(async (event) => {
  const body = await readBody<Form>(event)
  const html = await render(EmailTemplate, body)

  return await sendEmail({
    to: 'web@jagu.cz',
    subject: 'Nový zájemce o služby',
    html,
  })
})

sendEmail returns { ok: true, provider, dryRun? }. In development (or with NUXT_EMAIL_DRY_RUN=true) nothing is actually sent — the message is logged and dryRun: true is returned, so you don't need the old if (NODE_ENV === 'development') return guard in your handlers. Set NUXT_EMAIL_DRY_RUN=false to send for real from development.

Configuration

Select a default provider and add credentials via environment variables:

Variable Description
NUXT_EMAIL_PROVIDER Default provider: zimbra, smtp, or a custom name
NUXT_EMAIL_DEFAULT_FROM_NAME Sender display name used when a message has no explicit from
NUXT_EMAIL_DRY_RUN true/false; empty falls back to dev mode

Zimbra

NUXT_EMAIL_ZIMBRA_EMAIL=web@jagu.cz
NUXT_EMAIL_ZIMBRA_PASSWORD=your-password

SMTP

NUXT_EMAIL_SMTP_HOST=smtp.example.com
NUXT_EMAIL_SMTP_PORT=587
NUXT_EMAIL_SMTP_USERNAME=your-username
NUXT_EMAIL_SMTP_PASSWORD=your-password
NUXT_EMAIL_SMTP_EMAIL=noreply@example.com

sendEmail(message, options?)

message is either a direct message (explicit to + subject) or a templated message (template + optional overrides). Provide at least one body — html, text, or both.

Field Type Notes
to string | EmailAddress | Array<…> Required unless a template provides it
subject string Required unless a template provides it
html string HTML body — at least one of html / text is required
text string Plain-text body — at least one of html / text is required
from string | EmailAddress Defaults to the provider account + NUXT_EMAIL_DEFAULT_FROM_NAME
cc / bcc string | EmailAddress | Array<…> Optional
replyTo string | EmailAddress Optional
attachments EmailAttachment[] { filename, content: Buffer | string, contentType? }
template EmailTemplate A defineEmailTemplate preset (see below)
variables inferred Required & typed when the template's subject is a function

options.provider overrides the provider for a single call:

await sendEmail({ to: 'a@x.cz', subject: 'Hi', html }, { provider: 'smtp' })

Contacts

Author reusable Name + Email identities once and reference them directly — no retyped literals.

Define them in a shared file (auto-imported from server/utils):

// server/utils/emails.ts
export const CONTACTS = {
  Web: defineEmailContact({ name: 'Jagu', email: 'web@jagu.cz' }),
  Support: defineEmailContact({ name: 'Jagu Support', email: 'support@jagu.cz' }),
}

Then reference them when you send — in a route handler:

// server/api/contact.post.ts
export default defineEventHandler(async () => {
  return await sendEmail({ to: CONTACTS.Web, subject: 'Hi', html })
})

Templates

A template is a reusable preset of from / subject / cc / bcc / replyTo / provider — not the HTML. Explicit fields on a sendEmail call override the template.

Define them alongside your contacts:

// server/utils/emails.ts
export const TEMPLATES = {
  Inquiry: defineEmailTemplate({
    from: CONTACTS.Web,
    to: CONTACTS.Web,
    subject: 'Nový zájemce o služby',
    provider: 'zimbra',
  }),
  // A function subject makes `variables` required and type-checked at the call site.
  Order: defineEmailTemplate({
    from: CONTACTS.Web,
    subject: (vars: { id: number }) => `Order #${vars.id}`,
  }),
}

Use them from a handler — the template fills in the preset, you supply the body:

// server/api/order.post.ts
export default defineEventHandler(async () => {
  await sendEmail({ template: TEMPLATES.Inquiry, html })
  await sendEmail({ template: TEMPLATES.Order, variables: { id: 42 }, html })
})

Adding a provider

A provider implements one method: send(message). Register it on server startup and augment the registry interface so its name is type-safe everywhere.

// server/plugins/email.ts
declare global {
  interface EmailProviderRegistry {
    resend: true
  }
}

export default defineNitroPlugin(() => {
  registerEmailProvider(
    'resend',
    defineEmailProvider({
      async send(message) {
        // message.to/cc/bcc are EmailAddress[]; subject/html are resolved; attachments default to [].
        await myResendCall(message)
      },
    })
  )
})

Now NUXT_EMAIL_PROVIDER=resend, { provider: 'resend' }, and provider: 'resend' in a template are all autocompleted and type-checked.

Upgrading from v2

v3 is a clean break: the old sendZimbraEmail(...) / sendSmtpEmail(...) positional functions are gone, replaced by a single sendEmail({ ... }). Work through these steps:

1. Bump the dependency to ^3.0.0 and reinstall.

2. Convert every call site. Find them with grep -rn 'sendZimbraEmail\|sendSmtpEmail' server, then rewrite each from positional args to a message object. The provider is no longer baked into the function name — it comes from NUXT_EMAIL_PROVIDER, or pass { provider } to pin one:

// v2
await sendZimbraEmail('Jagu', 'web@jagu.cz', 'Subject', template, [])

// v3
await sendEmail(
  { from: { name: 'Jagu', email: 'web@jagu.cz' }, to: 'web@jagu.cz', subject: 'Subject', html: template },
  { provider: 'zimbra' },
)

3. Rename env vars in every deployment — they're now namespaced under EMAIL:

v2 v3
NUXT_ZIMBRA_EMAIL NUXT_EMAIL_ZIMBRA_EMAIL
NUXT_ZIMBRA_PASSWORD NUXT_EMAIL_ZIMBRA_PASSWORD
NUXT_SMTP_HOST NUXT_EMAIL_SMTP_HOST
NUXT_SMTP_PORT NUXT_EMAIL_SMTP_PORT
NUXT_SMTP_USERNAME NUXT_EMAIL_SMTP_USERNAME
NUXT_SMTP_PASSWORD NUXT_EMAIL_SMTP_PASSWORD
NUXT_SMTP_EMAIL NUXT_EMAIL_SMTP_EMAIL

4. Set NUXT_EMAIL_PROVIDER (zimbra or smtp) in each deployment to pick the default provider.

5. Delete manual dev guards. Remove any if (process.env.NODE_ENV === 'development') return from your handlers — the built-in dry-run (on by default in dev) replaces them.

Development

The .playground directory is a regular Nuxt app that extends this layer.

pnpm install
pnpm dev        # boot .playground on http://localhost:3000
pnpm typecheck
pnpm lint

Distributing

Bump the version, confirm files in package.json, then:

npm publish --access public

Version History

Version Released Vue License
3.0.1 latest
3.0.0 latest
2.0.2 latest
2.0.1 latest
2.0.0 latest