Self-hosted receive-only inboxes for OTP testing, QA automation, and private disposable mail workflows.
English · 中文
|
|
|
|
Legacy Mail Service is a small, production-oriented Node.js service for receiving email on domains you control. It stores inbound messages locally, provides a polished Mailbridge admin UI, and exposes API endpoints that make it easy to create test inboxes and read verification codes from E2E automation.
It is intentionally receive-only. It does not send email, relay outbound SMTP, host IMAP mailboxes, or try to replace Mailcow. It is built for private disposable inboxes, signup testing, OTP capture, QA runs, and developer tooling.
- Receive inbound email through a built-in SMTP server.
- Create disposable addresses under your own domains.
- Use the admin UI at
/uito inspect mail, unread state, categories, and extracted OTP codes. - Read messages from Playwright, Puppeteer, Cypress, shell scripts, or backend jobs through a small token-authenticated API.
- Keep the
codex-registrar-litelegacy provider contract throughPOST /admin/new_address. - Group inboxes manually or through rule-based categories.
- Render HTML email safely in sandboxed iframes while blocking scripts.
- Restrict the admin/API surface by fixed client IP allowlists.
- Deploy with Docker Compose or systemd behind Caddy/Nginx.
- Validate deployments with included health, DNS, and smoke-test scripts.
Use Legacy Mail Service when you need:
- Private temporary inboxes instead of public disposable-mail services.
- A stable domain for account registration and login-code testing.
- Automated verification-code polling in browser tests.
- A lightweight inbound-only mail sink for staging and QA.
- A self-hosted service that is easy to audit and operate.
Do not use it as a general mailbox platform. If you need real users, IMAP, outbound SMTP, spam filtering, aliases, calendars, or full mail administration, use a complete mail suite instead.
Internet email
|
v
DNS MX record -> VPS port 25 -> Legacy Mail SMTP receiver
|
v
local JSON store
|
+-------------------+-------------------+
| |
v v
Mailbridge admin UI JSON APIs
/ui /admin, /api
The service keeps the operational model deliberately small:
- One Node.js process runs the HTTP API/admin UI and SMTP receiver.
- Mail data is persisted in a local JSON store under
DATA_DIR. - HTTPS is handled by your reverse proxy, usually Caddy or Nginx.
- SMTP is exposed on port
25; the API/UI can stay on localhost behind the proxy.
git clone https://github.com/6mmLIU/legacy-mail-service.git
cd legacy-mail-service
cp .env.example .envEdit .env before starting:
ADMIN_USERNAME=admin
ADMIN_PASSWORD=replace-with-a-long-random-password
TOKEN_SECRET=replace-with-another-long-random-secret
ALLOWED_DOMAINS=example.comStart the service:
docker compose up -d --buildOpen the admin UI:
http://127.0.0.1:3000/ui
Check health:
LEGACY_MAIL_API_BASE_URL=http://127.0.0.1:3000 npm run healthcheckThe Compose file maps host SMTP port 25 to container port 2525, binds the HTTP UI/API to 127.0.0.1:3000, and stores mail in the legacy-mail-data Docker volume.
For development, use a high SMTP port so you do not need root privileges:
npm install
ADMIN_USERNAME=admin \
ADMIN_PASSWORD=dev-password \
TOKEN_SECRET=dev-secret \
ALLOWED_DOMAINS=example.test \
SMTP_PORT=2525 \
npm startThen open:
http://127.0.0.1:3000/ui
Point the mail host and MX record to your server:
A mail.example.com <server-ip>
MX example.com mail.example.com priority 10
Validate from your workstation or server:
LEGACY_MAIL_DOMAIN=example.com \
LEGACY_MAIL_MX_HOST=mail.example.com \
bash scripts/check-domain-dns.shExpose these public ports:
25/tcp inbound SMTP
80/tcp HTTP challenge or redirect
443/tcp HTTPS admin/API
Keep 3000/tcp private unless you intentionally want to expose the raw Node.js server.
Copy the repository to /opt/legacy-mail, create /etc/legacy-mail.env from .env.example, then install dependencies:
cd /opt/legacy-mail
npm install --omit=dev --ignore-scripts
sudo cp deploy/legacy-mail.service /etc/systemd/system/legacy-mail.service
sudo systemctl daemon-reload
sudo systemctl enable --now legacy-mailVerify locally:
curl -fsS http://127.0.0.1:3000/healthCaddy example:
mail-api.example.com {
reverse_proxy 127.0.0.1:3000
}Set TRUST_PROXY=true when the reverse proxy should provide the real client IP through X-Forwarded-For.
After the service is running:
LEGACY_MAIL_API_BASE_URL=https://mail-api.example.com \
LEGACY_MAIL_ADMIN_PASSWORD=<ADMIN_PASSWORD> \
LEGACY_MAIL_TEST_DOMAIN=example.com \
LEGACY_MAIL_SMTP_HOST=mail.example.com \
LEGACY_MAIL_SMTP_PORT=25 \
npm run smokeCreate an address:
curl -sS https://mail-api.example.com/admin/new_address \
-H "content-type: application/json" \
-H "x-admin-auth: $ADMIN_PASSWORD" \
-d '{"name":"openai-login","domain":"example.com"}'Read messages with the returned token:
curl -sS "https://mail-api.example.com/api/mails?limit=10" \
-H "authorization: Bearer $ADDRESS_TOKEN"Typical mail records include subject, from, to, text, html, unread, and verificationCode.
Core endpoints:
| Endpoint | Purpose |
|---|---|
GET /health |
Runtime health and non-secret counts |
POST /admin/new_address |
Create an address and token |
GET /api/mails |
List mails for a token or admin filter |
POST /api/mails/:id/read |
Mark a message as read |
DELETE /api/mails/:id |
Delete a message |
GET /openapi.json |
OpenAPI metadata |
See docs/api.md for the full reference.
| Variable | Default | Description |
|---|---|---|
API_HOST |
0.0.0.0 |
HTTP bind host |
API_PORT |
3000 |
HTTP bind port |
SMTP_PORT |
25 |
SMTP receiver port |
DATA_DIR |
./data |
Persistent store directory |
ADMIN_USERNAME |
admin |
Admin UI username |
ADMIN_PASSWORD |
required | Admin UI/API password |
SITE_PASSWORD |
empty | Optional extra site password |
TOKEN_SECRET |
ADMIN_PASSWORD fallback |
JWT/HMAC signing secret |
ALLOWED_DOMAINS |
* |
Comma-separated accepted recipient domains |
TRUST_PROXY |
false |
Trust X-Forwarded-For from a proxy |
SESSION_TTL_MS |
43200000 |
Admin session lifetime |
MAX_HTTP_BODY_BYTES |
262144 |
Maximum HTTP request body size |
MAX_MAIL_BYTES |
10485760 |
Maximum accepted SMTP message size |
server.js Node.js HTTP UI/API and SMTP receiver
docker-compose.yml Docker deployment
deploy/legacy-mail.service systemd unit
scripts/ DNS, health, smoke, and deployment helpers
docs/api.md API reference
docs/deployment.md Deployment guide
docs/dns.md DNS and MX guide
docs/automation.md Automation examples
specs/000-open-source-foundation Product spec and implementation plan
- Replace
ADMIN_PASSWORDandTOKEN_SECRETbefore production. - Keep the raw API/UI port private and put HTTPS in front of it.
- Use the fixed IP allowlist when the admin panel should only be reachable from known client IPs.
- The service receives mail only. It does not relay outbound SMTP.
- HTML mail is rendered in a sandboxed iframe and scripts are blocked.
- Treat the JSON store as sensitive because it contains received email content and tokens.
MIT. See LICENSE.


