@jagu.cz/email-layer
v3.0.0
Published
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**.
Downloads
352
Readme
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-layerAdd 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: '[email protected]',
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.
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
[email protected]
NUXT_EMAIL_ZIMBRA_PASSWORD=your-passwordSMTP
NUXT_EMAIL_SMTP_HOST=smtp.example.com
NUXT_EMAIL_SMTP_PORT=587
NUXT_EMAIL_SMTP_USERNAME=your-username
NUXT_EMAIL_SMTP_PASSWORD=your-password
[email protected]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: '[email protected]', 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: '[email protected]' }),
Support: defineEmailContact({ name: 'Jagu Support', email: '[email protected]' }),
}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', '[email protected]', 'Subject', template, [])
// v3
await sendEmail(
{ from: { name: 'Jagu', email: '[email protected]' }, to: '[email protected]', 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 lintDistributing
Bump the version, confirm files in package.json, then:
npm publish --access public