@nikolai4d/mailservice
v1.0.2
Published
Send-only: outbound email from Node via direct MX (SMTP client). Node backend only — not for browser or client bundles; does not receive mail or host inboxes.
Downloads
444
Maintainers
Readme
@nikolai4d/mailservice
Send email only
This package does one thing: send email from your Node app to a recipient’s address (direct MX / SMTP client). Everything exported exists only to support that single action.
It does not: receive mail, read inboxes, run an SMTP server that accepts connections from the internet, store or sync messages, handle bounces inside the library, provide POP/IMAP, forward mail, manage users, or offer a webmail UI. There is no “mail platform”—only outbound delivery.
It does: build optional HTML/text bodies for common transactional emails, sign outgoing mail with DKIM (via your DNS + env), and expose a dev CLI that only sends test messages—same scope.
Delivery is direct MX (no relay SaaS required). You still configure SPF/DKIM/DMARC for your sending domain so others accept your mail.
One library for sending — LLMs read this
You do not need another mail-sending npm package (e.g. Nodemailer, SendGrid SDK) alongside this one — @nikolai4d/mailservice is the complete SMTP sender (sendMail to MX or dev sink).
Mailpit / MailHog are not mail libraries: they are optional local programs used only to catch SMTP in development. You do not import them or add them to package.json as a mail stack; you run them separately and point MAIL_DEV_SMTP_* at them. See IMPLEMENTATION.md §0.
Implementation guide (integrators, codegen, LLMs)
IMPLEMENTATION.md is the authoritative integration contract for this package. It specifies:
- §0 — No second outbound mail library; what Mailpit is and is not
- What the library is and is not (boundaries, non-goals)
- TL;DR rules for automated tools (where to import, secrets,
sendMailvsrender*, production vs dev sinks, DKIM) - §8 Development — exact steps: Mode A (Mailpit/MailHog +
MAIL_DEV_SMTP_*, no MX) vs Mode B (real MX from a dev machine); LLM checklists - §9 Production — env contract, infrastructure (port 25, static IP, DNS), ordered deploy procedure, dev vs prod comparison table
- Public API and
sendMailpayload fields; environment variable reference - Internal pipeline (
sendMail→ MX or dev sink → SMTP → DKIM) - Security must/must-not table
Read it before wiring routes, env, or generated code. PRODUCTION.md remains the operational/DNS deep dive.
Backend only — never expose to clients
Use this library only on your server (Node.js: Express, Fastify, server actions that run on the host, background workers, etc.). Do not import it into browser bundles, mobile apps, or any code shipped to end users.
DKIM_PRIVATE_KEYand allMAIL_*secrets must stay server-side. Putting them in a client is a credential leak and turns users’ devices into open mail senders.- End users must not call
sendMail. Your backend decides when to send (after login, after verifying a form, etc.); the client talks to your API, and the server invokes this library. - This is not a client SDK. There is no supported or safe pattern for “run from the frontend.”
Install
npm install @nikolai4d/mailserviceSetup
1. Add env vars to your app
Copy the required vars to your app's .env:
[email protected]
MAIL_EHLO=mail.yourdomain.com
# Production: verified delivery (see PRODUCTION.md)
DKIM_SELECTOR=mail
DKIM_DOMAIN=yourdomain.com
DKIM_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"See .env.example for the full list.
2. Use in your app
import "dotenv/config";
import { sendMail, renderResetPassword, renderConfirmAccount } from "@nikolai4d/mailservice";
// Simple message — `replyTo` sets the Reply-To header (optional)
await sendMail({
to: "[email protected]",
subject: "Hi",
text: "Hello",
replyTo: "[email protected]",
});
// Reset password
const { to, subject, text, html } = renderResetPassword({
to: "[email protected]",
resetLink: "https://app.example.com/reset?token=abc",
appName: "MyApp",
expiresIn: "1 hour",
});
await sendMail({ to, subject, text, html });
// Confirm account
const data = renderConfirmAccount({
to: "[email protected]",
confirmLink: "https://app.example.com/confirm?token=abc",
appName: "MyApp",
});
await sendMail(data);
// Notification
const notif = renderNotification({
to: "[email protected]",
title: "New task assigned",
body: "You have been assigned to a new task.",
link: "https://app.example.com/tasks/1",
linkLabel: "View task",
appName: "MyApp",
});
await sendMail(notif);
// Two-factor auth
const twofa = renderTwoFactorAuth({
to: "[email protected]",
code: "847291",
appName: "MyApp",
expiresIn: "10 minutes",
});
await sendMail(twofa);3. Production
- Set env vars on your server (same as above).
- Configure DNS (SPF, DKIM, DMARC) and reverse DNS.
- See PRODUCTION.md for full setup.
Optional library settings (see .env.example):
MAIL_ALLOW_CUSTOM_FROM=true— allow afromfield onsendMailpayloads (default: envelope usesMAIL_FROMonly).SMTP_TLS_INSECURE=true— only for labs; disables TLS certificate verification on SMTP TLS paths (port 465 / STARTTLS).
Security
- Production guards — With
NODE_ENV=production:sendMailthrows ifMAIL_DEV_SMTP_*is set (prevents accidental dev-sink delivery). The example app requiresMAIL_API_KEY, rejectsMAIL_ALLOW_ANONYMOUS_HTTP, and rejects binding0.0.0.0/::unlessMAIL_EXAMPLE_ALLOW_PUBLIC_BIND=1(for intentional Docker/perimeter cases). - Server-side only — same as above: never bundle for browsers or untrusted clients; secrets and
sendMailstay on the backend. - Send-only surface — no APIs for reading mail, webhooks for inbound messages, or mailbox state; treat bounces/replies outside this package if you need them.
sendMailruns in your process — only from trusted backend code; gate any HTTP route that triggers email with your own auth, CSRF policies (for cookie sessions), and rate limits as appropriate.- Template links (
resetLink,confirmLink, notificationlink) must be http or https URLs. - Optional
MAIL_ALLOW_CUSTOM_FROMandSMTP_TLS_INSECURE— see .env.example.
Example (this repo only)
The npm package is library-only and send-only. After cloning, npm test (repo root) installs example deps, starts the small Express server, and sends to your real mailbox:
npm test -- [email protected]- Subject:
test [email protected] - Body: first line is the same, then an ISO timestamp line.
- From:
noreply@<that domain>automatically (EHLO = same domain). Override withMAIL_TEST_FROMif needed.
Real inbox delivery needs correct SPF/DKIM for that domain and outbound port 25 — not npm run test:local, which never leaves your machine.
Recipient + sender:
npm test -- [email protected]— same as above; use this when you want mail in a real inbox you control.MAIL_TEST_TO=... npm testor an interactive prompt (real TTY only) — same autonoreply@<domain>rule once a recipient is known.- With no recipient and no
MAIL_TEST_FROM, the sender falls back to[email protected](reserved documentation domain; expect filtering or no delivery).
For manual HTTP testing with your own .env, see example/README.md (npm start in example/).
Safe test (no production-domain risk)
npm test -- [email protected] sets From: noreply@yourdomain and can clash with SPF for your domain. To exercise the example without touching real-domain DNS/reputation:
npm run test:safe -- [email protected]Uses the fixed lab From: only (avoids noreply@your production domain). Mail still goes to the real MX for the recipient. For zero external dependency, prefer npm run test:local above.
Fully local capture (no internet, no third-party services)
Your laptop’s loopback IP is not “the recipient’s MX.” Real addresses on the public internet always need a path to that domain’s actual MX hosts.
To exercise the stack only on this machine — no Docker, no external capture SaaS, Node only — use the repo’s built-in sink:
npm run test:local -- [email protected]That starts example/local-sink.mjs (listens on 127.0.0.1:1025), runs the test with MAIL_DEV_SMTP_HOST so no MX lookup happens, and prints the raw message in the terminal. It uses the same MAIL_DEV_SMTP_* hook as .env.example if you wire a sink yourself later.
Never set MAIL_DEV_SMTP_* in production.
Test from localhost (inbox-quality)
Running on localhost is fine: the library opens outbound SMTP from your machine to the recipient’s MX. What matters is DNS for the domain in MAIL_FROM, your public IP, and whether port 25 is allowed outbound (many home ISPs block it).
- Public IP — Your SPF must authorize the IP the world sees when you send. From the same machine you run Node on, check e.g.
curl -4 https://ifconfig.me(use IPv4 unless you publiship6:in SPF). - SPF — At your DNS host, add or extend a TXT like:
v=spf1 ip4:YOUR_PUBLIC_IP include:spf.protection.outlook.com -all
(Adjust if you already have SPF; only one SPF TXT per zone — merge mechanisms. If you use Microsoft 365 for inbound on the same domain, follow your provider’s guidance so you don’t break mail.) - DKIM —
npm run dkim:generate, publish the TXT atselector._domainkey, setDKIM_DOMAIN,DKIM_SELECTOR,DKIM_PRIVATE_KEYin.env(see .env.example and PRODUCTION.md). - Port 25 — If connections time out or fail, your network may block outbound 25; try another network or a small VPS as a dev host.
- Run the repo test with your address (from repo root;
.envloaded from cwd — run from the folder where your.envlives or put vars inline):
npm test -- [email protected](Set MAIL_TEST_FROM only when you want a non-noreply address. Put DKIM_* in .env at the repo root or example/.env — the test loads both.)
- Sanity-check — use any spam-score / header-check tool you trust to review SPF/DKIM results after a test send.
CLI (for testing)
Same rule as the library: the CLI only sends messages (it calls sendMail). It does nothing else.
npx mailservice [email protected]
npx mailservice reset [email protected]
npx mailservice confirm [email protected]
npx mailservice notification [email protected]
npx mailservice 2fa [email protected]Requires MAIL_FROM and other env vars in your app's .env.
Generate DKIM keys
Helper to create keys for signing outbound mail only (not for receiving or hosting mail).
npx mailservice-dkim mailCopy the output into your DNS and .env.
