@jagu.cz/email-layer
| Install | |
|---|---|
npm install @jagu.cz/email-layer |
|
| Latest Version: | 3.0.1 |
| Last Updated: | Jul 10, 2026 |
| Links: | npm |
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 | ||
| 1.0.4 | latest | ||
| 1.0.3 | latest | ||
| 1.0.2 | latest | ||
| 1.0.1 | latest | ||
| 1.0.0 | latest |