@davaux/mail
Zero-dependency SMTP email client for Davaux. SMTP and MIME are implemented directly over node:net/node:tls — no nodemailer or other dependency, consistent with @davaux/storage's hand-rolled S3 client. Supports STARTTLS, implicit TLS, AUTH LOGIN/PLAIN, multipart/alternative (text + HTML) messages, and a small defineTemplate helper for authoring hardcoded, typed email templates.
Installation
npm install @davaux/mail
Basic setup
import { createMailer } from '@davaux/mail'
const mailer = createMailer({
host: process.env.SMTP_HOST!,
port: Number(process.env.SMTP_PORT ?? 587),
secure: process.env.SMTP_SECURE === 'true', // true = implicit TLS (465); otherwise STARTTLS is used automatically when offered
auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASSWORD! },
from: process.env.MAIL_NO_REPLY!,
})
await mailer.send({
to: 'user@example.com',
subject: 'Welcome',
text: 'Hello!',
html: '<p>Hello!</p>',
})
Config
| Option | Type | Default | Description |
|---|---|---|---|
host | string | required | SMTP server hostname. |
port | number | required | SMTP server port. |
secure | boolean | port === 465 | true for implicit TLS. Plaintext connections upgrade via STARTTLS automatically when the server offers it. |
auth | { user, pass } | — | Optional — omit for unauthenticated relays. |
from | string | required | Default From address, used when a message doesn't set its own. |
connectionTimeoutMs | number | 10000 | Milliseconds before an idle connection attempt is aborted. |
Sending a message
interface MailMessage {
to: string | string[]
cc?: string | string[]
bcc?: string | string[]
from?: string
replyTo?: string
subject: string
text?: string
html?: string
headers?: Record<string, string>
}
send opens a fresh connection, delivers the message, and closes it. At least one to address is required, and the message must set text and/or html — send throws otherwise.
await mailer.send({
to: ['user@example.com', 'cc-fallback@example.com'],
subject: 'Invoice #4821',
text: 'Your invoice is attached.',
html: '<p>Your invoice is attached.</p>',
})
When both text and html are set, the message is sent as multipart/alternative.
Sending several messages
sendMany reuses a single SMTP connection across messages — useful for notification batches or admin digests — and isolates failures per-message. It returns a PromiseSettledResult<void>[] in the same order as the input:
const results = await mailer.sendMany(
admins.map((to) => ({ to, subject: 'Server alert', text: '...' })),
)
const failed = results.filter((r) => r.status === 'rejected')
Templates
defineTemplate is an identity helper for authoring hardcoded templates with an inferred data type — it exists so template declarations read cleanly and TData isn't written out twice:
import { createMailer, defineTemplate, sendTemplate } from '@davaux/mail'
const passwordReset = defineTemplate<{ displayName: string; resetUrl: string }>(
({ displayName, resetUrl }) => ({
subject: 'Reset your password',
text: `Hi ${displayName}, reset here: ${resetUrl}`,
html: `<p>Hi ${displayName}, <a href="${resetUrl}">reset your password</a>.</p>`,
}),
)
await sendTemplate(mailer, passwordReset, { displayName: 'David', resetUrl }, {
to: 'user@example.com',
})
sendTemplate(mailer, template, data, envelope) renders template with data and sends it, merging in the recipient/envelope fields (to, cc, bcc, from, replyTo, headers) from envelope.
Mailer API
| Method | Description |
|---|---|
mailer.send(message) | Sends one message over a fresh connection. |
mailer.sendMany(messages) | Sends several messages sequentially, reusing a single connection. Failures are isolated per-message. |
Why no 3rd party dependency?
This package implements SMTP and MIME directly over node:net/node:tls rather than depending on nodemailer or another client. It keeps the package dependency-free while covering the common cases: STARTTLS, implicit TLS, AUTH LOGIN/PLAIN, and multipart/alternative messages.