Reading the repo
Reading the repo
How to install on macOS
:mailbox: SMTP Server + Web Interface for viewing and testing emails during development.
GitHub didn't answer, so this is the saved copy from 12 h ago
Will it work on my device?
CONTRIBUTING.mdMailDev 3.0 is a pnpm + turbo monorepo. This guide covers local development and the release process.
The publishable packages live under packages/*:
| Package | npm name | Purpose |
| ------- | --------------- | --------------------------------------------------- |
| core | @maildev/core | Core types, utilities, and storage abstraction |
| smtp | @maildev/smtp | SMTP server |
| api | @maildev/api | REST API + WebSocket server |
| ui | @maildev/ui | React web interface |
| mcp | @maildev/mcp | MCP server for Claude integration |
| cli | maildev | The maildev CLI binary (bundles all of the above) |
The root package.json is private (legacy v2 code) and is not published.
>=2010.34.4 (pinned via packageManager)pnpm install # first time only
pnpm dev # turbo run dev — starts every package concurrently
pnpm dev launches each package's dev task at once:
@maildev/api → tsx watch src/dev.ts, booting the full backend (SMTP + REST API + WebSocket) in one process@maildev/ui → vite dev server for the React UI (runs on http://localhost:5173 vite port during development)core, smtp, cli → tsc --watch for incremental rebuildsDefault endpoints:
localhost:1025 (point your app's mail transport here)http://localhost:1080Send a test email to verify things work:
node scripts/send.js
Override ports/host via env vars (read in packages/api/src/dev.ts):
SMTP_PORT=2525 API_PORT=8080 API_HOST=127.0.0.1 pnpm dev
Run the built CLI binary instead of the dev server:
pnpm build
pnpm --filter maildev cli # runs dist/bin/maildev.js
| Command | What it does |
| ------------------- | ---------------------------------------------------------- |
| pnpm install | Install workspace dependencies |
| pnpm dev | Start all packages in watch mode |
| pnpm build | turbo run build — build every package |
| pnpm test | turbo run test — build, then run vitest in every package |
| pnpm typecheck | turbo run typecheck |
| pnpm lint | turbo run lint |
| pnpm lint:fix | Lint with --fix |
| pnpm format | Format all files with prettier |
| pnpm format:check | Check formatting without writing |
Tests run per package with vitest, orchestrated by turbo:
pnpm --filter @maildev/core test
pnpm --filter @maildev/core test:watch # re-run on change
pnpm --filter @maildev/core test:coverage # with coverage
Releases are managed with Changesets. All six packages are linked, so they version together, and publish with access: public. Internal workspace:* dependencies are rewritten to real versions at publish time.
Changelogs are generated with @changesets/changelog-github, so each entry links its originating PR and credits the author.
Log into npm (interactive):
npm login
Create a .env file in the repo root with a GitHub token (read access is enough — public_repo on a classic token, or a fine-grained token with Contents: Read on maildev/maildev):
# .env (git-ignored)
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx
@changesets/changelog-github calls the GitHub API during versioning to resolve PR numbers and author handles, and hard-requires GITHUB_TOKEN. The pnpm run version script loads .env via scripts/with-env.sh and fails fast if the file or token is missing, so a release can't silently produce a broken changelog.
Use
pnpm run version, notpnpm version.pnpm versionruns pnpm's built-in version command and skips the"version"script (and its.envguard) entirely. If you invokepnpm changeset versiondirectly, exportGITHUB_TOKENin your shell first.
⚠️ Always publish prereleases under a dist-tag. npm publish sets the latest tag by default — a semver prerelease suffix does not change that. The maildev package already serves a stable latest (2.x), so publishing an alpha without a tag would clobber it for everyone running npm install maildev.
If the versions are already set to the target prerelease (e.g. 3.0.0-alpha.0), just build and publish under the tag:
pnpm build
pnpm changeset publish --tag alpha
changeset publish skips the private root, skips anything already on the registry, and pushes the rest. Users then install with:
npm install maildev@alpha
latest stays on the stable release.
Use Changesets' pre-release mode so version bumps and tagging happen automatically:
pnpm changeset pre enter alpha # writes .changeset/pre.json
pnpm changeset # describe the change
pnpm run version # bumps 3.0.0-alpha.0 → alpha.1 (loads .env)
pnpm build
pnpm changeset publish # auto-uses the `alpha` tag from pre.json
git push --follow-tags # push commits + version tags
When you're ready to cut a stable release, exit pre-release mode first:
pnpm changeset pre exit
pnpm run version # e.g. 3.0.0-alpha.N → 3.0.0 (loads .env)
pnpm build
pnpm changeset publish # publishes to `latest`
git push --follow-tags # push commits + version tags
Commit the version bump (the updated package.jsons and CHANGELOG.mds) before publishing, so the pushed tag points at the changelog the release notes are extracted from.
Note: the repo's
pnpm releasescript (turbo run build && changeset publish && git push --follow-tags) publishes tolatestand pushes tags. Use the--tag/pre-mode flow above for prereleases so you don't overwrite the stablelatest.
The maildev/maildev image is built and published with the scripts in scripts/:
| Command | What it does |
| ------------------- | --------------------------------------------------------------------------------------- |
| pnpm docker-build | Build the image (multi-arch by default) and load it into the local Docker daemon |
| pnpm docker-run | Run the locally built image with ports 1080 (web UI) and 1025 (SMTP) mapped |
| pnpm docker-push | Build linux/amd64 + linux/arm64 and push the multi-arch manifest list to Docker Hub |
The image version is resolved from the git tag on the current commit by
scripts/dockerVersion.sh — not from package.json, which is only updated
when changeset version runs. It prefers the changesets tag (maildev@X.Y.Z),
falls back to any other tag pointing at HEAD (legacy vX.Y.Z → X.Y.Z),
and fails if HEAD is untagged, so an image can never be built or pushed
under a stale or unknown version. This slots the Docker push naturally after
the npm release, which creates the tag:
pnpm changeset publish # publishes to npm and tags HEAD (maildev@<version>)
git push --follow-tags
docker login # needs push access to maildev/maildev on Docker Hub
pnpm docker-push
docker-push runs a single docker buildx build --push — the registry is
the merge point for the multi-arch manifest list, so no separate docker push
is needed. The image is published as maildev/maildev:<version> and
:latest, with provenance and SBOM attestations and inline layer caching.
Notes:
HEAD: run the Docker scripts on the tagged release
commit. To build or push an arbitrary commit, tag it first
(e.g. git tag maildev@1.2.3).pnpm docker-build defaults to a multi-platform build with --load, which
requires the containerd image store (default on recent Docker Desktop). On
older setups, build a single platform: pnpm docker-build linux/arm64.qemu-user-static for arm64 emulation; Docker Desktop
ships with it. The docker-container driver builder used for multi-arch
builds (multiarch) is created automatically on first run.Pushing the maildev@<version> tag triggers the .github/workflows/draft-release.yml workflow, which extracts that version's section from packages/cli/CHANGELOG.md and opens a draft GitHub Release (marked pre-release while .changeset/pre.json exists). Review and publish it manually. Only the single maildev@* tag drives it — the five per-package tags are ignored, so each version drafts exactly one release.
Then run bash setup.sh. It stops at the first error and is safe to run again. Servers and tests aren't started; it lists them at the end. Read it first.
Needs Git: xcode-select --install
git clone https://github.com/maildev/maildev.git cd maildev
Node.js >=20.0.0
# needs nvm: brew install nvm (or https://github.com/nvm-sh/nvm) nvm install --lts
corepack enable pnpm install
pnpm dev
pnpm test
No reports for macOS yet.
Opens a chat with this manual already filled in, for macOS. It uses your own account; gitmanual sends nothing.