npm package: @nikolai4d/mailservice
Description: 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.
Author: Nikolai Development AB
Latest version: 1.0.0

# @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.

### 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_KEY` and all `MAIL_*` 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

```bash
npm install @nikolai4d/mailservice
```

## Setup

### 1. Add env vars to your app

Copy the required vars to your app's `.env`:

```env
MAIL_FROM=noreply@yourdomain.com
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](./.env.example) for the full list.

### 2. Use in your app

```js
import "dotenv/config";
import { sendMail, renderResetPassword, renderConfirmAccount } from "@nikolai4d/mailservice";

// Simple message — `replyTo` sets the Reply-To header (optional)
await sendMail({
  to: "user@example.com",
  subject: "Hi",
  text: "Hello",
  replyTo: "support@example.com",
});

// Reset password
const { to, subject, text, html } = renderResetPassword({
  to: "user@example.com",
  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: "user@example.com",
  confirmLink: "https://app.example.com/confirm?token=abc",
  appName: "MyApp",
});
await sendMail(data);

// Notification
const notif = renderNotification({
  to: "user@example.com",
  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: "user@example.com",
  code: "847291",
  appName: "MyApp",
  expiresIn: "10 minutes",
});
await sendMail(twofa);
```

### 3. Production

1. Set env vars on your server (same as above).
2. Configure DNS (SPF, DKIM, DMARC) and reverse DNS.
3. See [PRODUCTION.md](./PRODUCTION.md) for full setup.

**Optional library settings** (see [.env.example](./.env.example)):

- `MAIL_ALLOW_CUSTOM_FROM=true` — allow a `from` field on `sendMail` payloads (default: envelope uses `MAIL_FROM` only).
- `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`**: `sendMail` **throws** if **`MAIL_DEV_SMTP_*`** is set (prevents accidental dev-sink delivery). The **example** app **requires `MAIL_API_KEY`**, **rejects `MAIL_ALLOW_ANONYMOUS_HTTP`**, and **rejects binding `0.0.0.0` / `::`** unless **`MAIL_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 `sendMail` stay 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.
- **`sendMail` runs 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`, notification `link`) must be **http** or **https** URLs.
- Optional **`MAIL_ALLOW_CUSTOM_FROM`** and **`SMTP_TLS_INSECURE`** — see [.env.example](./.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**:

```bash
npm test -- user@yourdomain.com
```

- **Subject:** `test user@yourdomain.com`
- **Body:** first line is the same, then an ISO timestamp line.
- **From:** `noreply@<that domain>` automatically (EHLO = same domain). Override with **`MAIL_TEST_FROM`** if 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 -- you@yourdomain.com`** — same as above; use this when you want mail in a **real** inbox you control.
- **`MAIL_TEST_TO=... npm test`** or an interactive prompt (real TTY only) — same auto **`noreply@<domain>`** rule once a recipient is known.
- With **no** recipient and no **`MAIL_TEST_FROM`**, the sender falls back to **`test@example.invalid`** (reserved documentation domain; **expect filtering or no delivery**).

For manual HTTP testing with your own `.env`, see [example/README.md](./example/README.md) (`npm start` in `example/`).

### Safe test (no production-domain risk)

`npm test -- you@yourdomain.com` sets **From: noreply@yourdomain** and can clash with SPF for **your** domain. To exercise the example **without** touching real-domain DNS/reputation:

```bash
npm run test:safe -- you@someone.example
```

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:

```bash
npm run test:local -- anyone@example.test
```

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](./.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).

1. **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 publish `ip6:` in SPF).
2. **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.)
3. **DKIM** — `npm run dkim:generate`, publish the TXT at `selector._domainkey`, set `DKIM_DOMAIN`, `DKIM_SELECTOR`, `DKIM_PRIVATE_KEY` in `.env` (see [.env.example](./.env.example) and [PRODUCTION.md](./PRODUCTION.md)).
4. **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.
5. **Run the repo test with your address** (from repo root; `.env` loaded from cwd — run from the folder where your `.env` lives or put vars inline):

```bash
npm test -- you@yourdomain.com
```

(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.)

6. **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.

```bash
npx mailservice user@example.com
npx mailservice reset user@example.com
npx mailservice confirm user@example.com
npx mailservice notification user@example.com
npx mailservice 2fa user@example.com
```

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

```bash
npx mailservice-dkim mail
```

Copy the output into your DNS and `.env`.