DAVAUX
Alpha

@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

OptionTypeDefaultDescription
hoststringrequiredSMTP server hostname.
portnumberrequiredSMTP server port.
securebooleanport === 465true for implicit TLS. Plaintext connections upgrade via STARTTLS automatically when the server offers it.
auth{ user, pass }Optional — omit for unauthenticated relays.
fromstringrequiredDefault From address, used when a message doesn't set its own.
connectionTimeoutMsnumber10000Milliseconds 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 htmlsend 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

MethodDescription
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.