Email is deceptively hard: sending reliably, composing HTML that survives Outlook, and keeping a list free of typos and dead inboxes. MailKit turns all of it into a few clean calls.

Four packages

MailKit packages: mailer, email-templates, email-validate, email-verify
From the first welcome to a verified list.

Send in one line

import { createMailer, mailerFromEnv } from '@lacspace/mailer';
const mail = createMailer(mailerFromEnv()); // reads SMTP_* env vars
await mail.send({ to: user.email, subject: 'Welcome', html });

Keep your list clean

Validate at signup to catch gmial.com before it costs you a customer; verify periodically to prune dead addresses and protect your sender reputation. Both are one call.

Get started

Browse MailKit at lacspace.com/packages and read the API at lacspace.com/docs. It pairs naturally with the Security Kit for auth emails and the rest of the ecosystem.

Install

npm i @lacspace/mailer @lacspace/email-templates @lacspace/email-validate @lacspace/email-verify

Two of the four are server-only by design. @lacspace/mailer speaks SMTP over Node's built-in net/tls sockets and @lacspace/email-verify uses Node's dns and net, so both need Node 18 or newer and will not run in a browser. @lacspace/email-templates and @lacspace/email-validate are isomorphic and work anywhere, including a signup form in the client.

Sending: create a mailer, then send

In the package itself, send is a method on a mailer you create once from a config. Provider presets cover Hostinger, Gmail, Outlook/Office365, Zoho, Brevo, SMTP2GO and Mailgun, or you can build the config from environment variables.

import { createMailer, presets, mailerFromEnv } from '@lacspace/mailer';

const mail = createMailer(presets.gmail({ user, pass })); // use an App Password for Gmail
// or: createMailer(mailerFromEnv()) reads SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS, SMTP_FROM

await mail.verify(); // true if the server accepts the connection and credentials
await mail.send({
  from: 'Acme <no-reply@acme.com>',
  to: user.email,
  subject: 'Your invoice',
  html,
  attachments: [{ filename: 'invoice.pdf', content: pdfBuffer, contentType: 'application/pdf' }],
});

Implicit TLS on port 465 and STARTTLS on 587 are both supported, and send() resolves to { messageId, accepted, response }.

Bulk sending

For newsletters or notification fan-out, createMailerPool reuses a few connections and caps the send rate. sendBatch adds a concurrency limit and per-message retry with backoff, and returns a summary of what was sent and what failed.

import { createMailerPool, sendBatch, presets } from '@lacspace/mailer';

const pool = createMailerPool({ ...presets.brevo({ user, pass }), maxConnections: 5, rateLimit: 20, rateDelta: 1000 });
const summary = await sendBatch(pool, messages, { concurrency: 5, retries: 3 });
// { total, sent, failed, results: [...] }
await pool.close();

Composing: templates that survive email clients

@lacspace/email-templates returns HTML with inline styles wrapped in a responsive, dark-mode-aware layout. It ships ready-made templates (otpEmail, welcomeEmail, verifyEmail, passwordResetEmail, magicLinkEmail, invoiceEmail, orderConfirmationEmail and more) plus blocks for building your own.

import { render, heading, text, button, divider, otpEmail, toPlainText } from '@lacspace/email-templates';

const html = render(
  { title: 'Reset your password', preheader: 'Link expires in 30 minutes', brandName: 'Acme',
    theme: { brandColor: '#4d9fff' } },
  [
    heading('Reset your password'),
    text('Click the button below to choose a new password.'),
    button('Reset password', resetUrl),
    divider(),
    text("If you didn't request this, you can ignore this email.", { muted: true }),
  ],
);

await mail.send({ to, subject: 'Your code', html: otpEmail({ code: '482913', expiresMinutes: 10 }),
  text: toPlainText(html) });

Buttons are table-based "bulletproof" CTAs, untrusted text is escaped automatically, and interpolate() / localize() fill {{var}} copy for translated emails.

Validating at signup

validateEmail does syntax and length checks, flags disposable and role addresses, suggests fixes for common domain typos and returns a normalized form you can use for de-duplication. It needs no network access.

import { validateEmail } from '@lacspace/email-validate';

const r = validateEmail(input);
if (!r.valid) return fail('Please enter a valid email.');
if (r.suggestion) return confirm(`Did you mean ${r.suggestion}?`); // gmial.com -> gmail.com
if (r.disposable) return fail('Please use a permanent email address.');
await createUser({ email: r.normalized });

Normalization lowercases the address and, for Gmail, strips dots and +tags, so John.Doe+promo@GMAIL.com becomes johndoe@gmail.com. Each rule can be switched off through the options of normalizeEmail.

Verifying an inbox

@lacspace/email-verify resolves MX records and can optionally run an SMTP RCPT TO probe to ask the receiving server whether the mailbox exists, without sending a message.

import { verifyEmail, verifyBatch } from '@lacspace/email-verify';

await verifyEmail(email, { checkSmtp: false }); // MX-only: fast, no port 25 needed
const verdicts = await verifyBatch(emails, { concurrency: 10, detectCatchAll: true });
// each result carries confidence: { score, risk, reasons }

How it compares to wiring it yourself

  • Sending: the usual route is an SMTP library plus a separate MIME and templating stack. Here the MIME builder (createMessage(), with inline CID images and autoText()), address parsing and RFC 2047 header encoding are part of the mailer, with no npm dependencies.
  • Templates: the email-templates README describes itself as a small, dependency-free alternative to MJML. You write JavaScript function calls instead of a markup language and a compile step.
  • Testing: createMemoryTransport() implements the same interface as a real mailer and records messages in .messages, so tests can assert on the rendered MIME without opening a socket.

Gotchas

  • SMTP probing is unreliable by nature. Many servers greylist, accept every address (catch-all) or block probes, so unknown is a common result and a positive means "likely deliverable". The README advises using it to catch typos and dead domains, not as a hard gate.
  • Port 25 is often blocked on cloud hosts and most serverless platforms. Use { checkSmtp: false } there.
  • Serverless and sending: because the mailer opens TCP sockets, deploy it where outbound SMTP connections are allowed.
  • Local development: point createMailer at a local catcher such as MailHog with { host: '127.0.0.1', port: 1025, secure: false, ignoreTLS: true }.

Package documentation: mailer, email-templates, email-validate and email-verify. On npm: @lacspace/mailer, @lacspace/email-templates, @lacspace/email-validate and @lacspace/email-verify. All four are released under the Lacspace Free Licence v1.0.

Frequently asked questions

What is MailKit?

MailKit is Lacspace’s email suite for backends: @lacspace/mailer (send with one line), @lacspace/email-templates (compose responsive HTML), @lacspace/email-validate (syntax + MX checking) and @lacspace/email-verify (confirm a real inbox). Zero dependencies; the mailer and email-verify packages run server-side on Node.

How do I send an email?

Create a mailer once with `createMailer(mailerFromEnv())` (or a provider preset), then call `mail.send({ to, subject, html })`. It handles SMTP/providers and retries so a transient failure doesn’t drop the message.

How is validate different from verify?

Validate is a fast, offline-ish check — is the address well-formed and does the domain have MX records — great for catching typos at signup. Verify goes further to confirm the mailbox actually exists, so your list stays clean and your sender reputation stays high.

Can I build nice-looking emails?

Yes — @lacspace/email-templates composes responsive HTML email that renders across clients, so your welcome and transactional emails look intentional, not like plain text.