Open-source, zero-storage custom domain email delivery infrastructure.
Mulul Mail puts a thin, type-safe Next.js (App Router) API gateway in front of
the Stalwart mail core (a modern, high-performance mail
server written in Rust). The gateway accepts JSON over HTTP, verifies that the
sender's domain provably belongs to you (live SPF/DKIM DNS checks), maps the
payload into native JMAP (Email/set + EmailSubmission/set), relays it over
SMTP, and purges the message the instant submission succeeds. No mailboxes,
no archive, no plaintext left behind.
client ──JSON──▶ app-gateway (Next.js) ──JMAP──▶ Stalwart ──SMTP──▶ recipient
│ verify domain │ sign (DKIM)
│ map → Email/set │ relay
└ purge references └ onSuccessDestroyEmail
.
├── compose.yml # Production orchestration (stalwart + app-gateway)
├── compose.dev.yml # Local dev override (mocked DNS, live reload)
├── .env.example # Documented environment template
├── setup.sh # One-command live-host bootstrap
└── app-gateway/ # Next.js gateway + portal
├── Dockerfile # Multi-stage standalone build
├── next.config.js # CORS + /jmap reverse proxy
└── src/
├── app/
│ ├── api/domains/route.ts # Multi-tenant domains + DKIM
│ ├── api/send/route.ts # Ephemeral JMAP relay
│ ├── api/health/route.ts # Liveness probe
│ └── page.tsx # Enterprise control portal
└── lib/ # env, auth, dns, stalwart JMAP client
git clone <your-fork> mulul-mail && cd mulul-mail
chmod +x setup.sh && ./setup.sh # interactive: writes .env, builds, bootssetup.sh checks for Docker + the Compose plugin, generates a .env (prompting
for your mail hostname, public IP, and admin credentials, auto-generating
secrets), and starts the stack.
Then open http://<server-ip>:3000 for the portal.
cp .env.example .env
docker compose -f compose.yml -f compose.dev.yml up --buildIn dev mode (NODE_ENV=development) all domain/SPF/DKIM verification returns
mocked passing results so you can exercise the full send pipeline without
owning DNS. Source is bind-mounted with live reload.
All endpoints require Authorization: Bearer <GATEWAY_API_KEY>.
Response: { ok, submissionId, domain, storage: "purged", ... }.
POST body { "domain": "acme.com", "selector": "default" } registers the domain
in Stalwart, provisions DKIM keys, and returns the SPF/DKIM/DMARC records to
publish. GET lists all domains with a live verification report.
| Port | Service | Purpose |
|---|---|---|
| 25 | Stalwart | SMTP inbound + outbound relay |
| 465 | Stalwart | SMTPS (implicit TLS) |
| 587 | Stalwart | Submission (STARTTLS) |
| 8080 | Stalwart | JMAP / management API (internal) |
| 3000 | app-gateway | Portal + REST API |
- DNS:
Arecord forMAIL_DOMAIN→ server IP, plus matching PTR/rDNS. - Open egress on port 25 (many cloud providers block it by default).
- Publish per-tenant SPF, DKIM and DMARC records (see
POST /api/domains). - Front the gateway with TLS (reverse proxy / Caddy / nginx) before exposing 3000.
- Keep
.env(chmod 600) out of version control — it is git-ignored.
MIT.
{ "from": "[email protected]", // domain must be verified "to": ["[email protected]"], "cc": [], "bcc": [], "subject": "Hello", "text": "Plain body", // text and/or html "html": "<p>Rich body</p>", "replyTo": "[email protected]" }