Skip to content
Alexis-TechnologiesPublic

About

Zero-dependency email for Node.js, Bun and Deno: its own SMTP client, sendmail, MIME builder and DKIM, eighteen HTTP providers and JMAP, a persistent queue with a BullMQ driver, templates over nine engines, an audit of message states, verified webhooks into one model of delivery events, and decorators for failover, retries, circuit breaking & more

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

mailer

npm CI node dependencies docs license

Email for Node.js, with a reliability layer. Its own SMTP client, MIME builder, DKIM signing and eighteen HTTP providers, and on top of them what the other libraries leave to the application: a persistent queue, on BullMQ or a transactional outbox in MongoDB, whose workers render templates of the engine you use and know a permanent failure from a temporary one, an audit of every state of a message, one model of delivery events and idempotency; next suppression. Zero runtime dependencies.

Status: 1.2 is the transport layer and the reliability layer on it. 1.0 ported the transport from upyo 0.7.0; 1.1 adds what nodemailer, unemail and sently did better: sendmail, ten more HTTP providers, attachments from paths and URLs, one-click List-Unsubscribe, verified webhooks into one model of delivery events, and decorators for circuit breaking, rate limits, idempotency, hooks and mirroring. 1.2 adds the queue (./queue, with a BullMQ driver in ./bullmq and a transactional outbox in ./mongodb), templates by name over nine engines (./templates) and the audit of the states of a message (./audit). Suppression is next.

Why @alexify/mailer

  • Its own transport. An SMTP client (pool, STARTTLS, PIPELINING, DSN, SMTPUTF8, OAuth 2.0, an HTTP CONNECT proxy or any socket you connect), sendmail, a MIME builder (IDN domains as punycode, 7bit, quoted-printable or base64 by content) and DKIM (RSA-SHA256, Ed25519), JMAP, and eighteen HTTP providers on fetch: Amazon SES v2, Resend, SendGrid, Mailgun, Postmark, Brevo, Mailjet, MailerSend, SparkPost, Mailtrap, Maileroo, Lettermint, ZeptoMail, SMTP2GO, Azure Communication Services, Scaleway, Cloudflare and Plunk. Secure by default: TLS certificates are verified, AUTH never goes over plain text, CRLF in a header is rejected, and no file or URL is read for an attachment unless you ask.
  • What happens after send(). The webhooks of fifteen providers, each verified before it is read and taken as handled only once your handler commits it, give one model of delivery events (delivered, bounced, complained, …). A queue on BullMQ or a store of yours sends each message once per key, retries what may pass and fails what will not, and an audit sink hears every state from queued to bounced; in MongoDB, the job is written in the transaction of its cause. Next: suppression. None of the fourteen libraries compared has them.
  • Sent once, or known to be unknown. Errors are classified as permanent or temporary, and a failure after which the provider may have taken the message is marked deliveryUnknown: ./retry and ./pool never send it again. ./idempotency reserves a key before the send, ./breaker lets one probe through a failing provider, and ./ratelimit keeps to its quota.
  • Correct before broad. The tests read the composed messages back with an independent parser and check DKIM with an independent verifier, and the suites run on Node 22 and 24, Bun and Deno; golden .eml messages and fuzzing of headers, multipart/related, dot-stuffing and internationalized addresses are planned.
  • Zero dependencies. Built on node:* modules and fetch. What a part needs from your application is passed in: the OpenTelemetry providers, a pino-like logger, BullMQ's classes, your template engine (EJS, Handlebars, Pug, MJML, Liquid, Nunjucks, Eta, Mustache, React Email) and the collections of MongoDB. CommonJS with ESM named imports and hand-written TypeScript typings, no build step.

Installation

pnpm add @alexify/mailer
# or
npm install @alexify/mailer

Requires Node.js 22 or newer. Bun and Deno run it too, tested in CI; on Deno, see Bun and Deno for the permissions and the fetch to pass.

Usage

The root entry builds messages; each transport is a subpath of the same package:

const { createMessage } = require('@alexify/mailer');
const { SmtpTransport } = require('@alexify/mailer/smtp');

const transport = new SmtpTransport({
  host: 'smtp.example.com',
  port: 587,
  auth: { user: 'user@example.com', pass: 'secret' },
});

const receipt = await transport.send(
  createMessage({
    from: 'Alex <alex@example.com>',
    to: 'bob@example.net',
    subject: 'Hello',
    content: { text: 'Hi there.', html: '<p>Hi there.</p>' },
  }),
);
if (!receipt.successful) console.error(receipt.errorMessages);
Subpath What it is
@alexify/mailer messages, attachments, calendar parts, the Transport contract, receipts, delivery events
/mime MIME composition and DKIM signing
/attachments, /render attachments from a path, a URL, a data URI or a stream; the content of a renderer, React Email
/templates templates by name over EJS, Handlebars, Pug, MJML, Liquid, Nunjucks, Eta, Mustache or React Email: locales, layouts, subjects, CSS inlined
/queue, /bullmq, /mongodb, /audit the mail queue and its worker, the BullMQ driver, the MongoDB outbox (and its audit and idempotency stores), the states of a message and their sinks
/smtp, /sendmail, /jmap SMTP, sendmail and JMAP transports
/ses, /resend, /sendgrid, /mailgun, /postmark, /brevo, /mailjet, /mailersend, /sparkpost, /mailtrap, /maileroo, /lettermint, /zeptomail, /smtp2go, /azure, /scaleway, /cloudflare, /plunk HTTP providers
/webhooks verified webhooks of fifteen providers, read into delivery events
/pool, /retry, /breaker, /ratelimit, /idempotency, /hooks, /tee, /otel, /logger decorators: failover, retries, circuit breaking, rate limits, idempotency, hooks, mirroring, OpenTelemetry, logging
/mock an in-memory transport for tests

Every transport takes generateId for the identifiers it makes (a ULID or a cuid2 instead of a UUID) and a pino-like logger for what happens inside it, and every HTTP transport a fetch, such as one bound to an undici Agent or a proxy; see your own fetch and identifiers.

Through a queue, the worker renders the template and sends the message, and the queue retries what may pass:

const { Queue, Worker } = require('bullmq');
const ejs = require('ejs');
const { createMailQueue } = require('@alexify/mailer/queue');
const { createBullMQDriver } = require('@alexify/mailer/bullmq');
const { createEjsEngine, createFileLoader, createTemplateRenderer } = require('@alexify/mailer/templates');

const queue = createMailQueue({
  driver: createBullMQDriver({ bullmq: { Queue, Worker }, connection: { host: 'localhost' } }),
  // Whoever can write to Redis cannot make the worker send a message of theirs.
  signingKeys: [{ id: '2026-10', secret: process.env.MAIL_JOB_KEY }],
});
await queue.enqueue({
  kind: 'template',
  template: 'reset-password', // emails/reset-password.ejs, its subject in its front matter
  context: { url },
  from: 'no-reply@example.com',
  to: user.email,
  idempotencyKey: `reset-password:${resetRequest.id}`, // an id, never an address or a token
});

queue.createWorker({
  transport,
  renderer: createTemplateRenderer({
    engine: createEjsEngine({ ejs }),
    loader: createFileLoader({ dir: 'emails' }),
  }),
});

A job holds its message only until it is done with (retainData), and may be sealed by a codec of yours; a job that kills its worker is given up rather than sent round for ever; tenants can take turns (fairness: 'tenant' on the outbox), and instrumentHandler of /otel gives the worker its spans and metrics. See the queue guide.

The same names import from ESM (import { SmtpTransport } from '@alexify/mailer/smtp'). The API reference lists every member, and the guide every transport.

Compared with alternatives

The comparison puts Nodemailer, emailjs, Upyo, unemail, sently, worker-mailer, edge-mailer, MIMEText and the SDKs of Resend, SendGrid, Mailgun, Postmark, Mailtrap and Amazon SES side by side, feature by feature, from their source code.

Credits

The transport layer is a port of upyo 0.7.0 by Hong Minhee (MIT), converted to CommonJS with hand-written types and no dependencies. Features of 1.1 are ported from nodemailer 10.0.16 by Andris Reinman (MIT-0), unemail 0.7.0 (MIT) and sently 1.2.2 (MIT), with their known bugs fixed in the port. Their notices are in LICENSE.

Changelog

See CHANGELOG.md.

License

MIT Β© Alexis Technologies

About

Zero-dependency email for Node.js, Bun and Deno: its own SMTP client, sendmail, MIME builder and DKIM, eighteen HTTP providers and JMAP, a persistent queue with a BullMQ driver, templates over nine engines, an audit of message states, verified webhooks into one model of delivery events, and decorators for failover, retries, circuit breaking & more

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages