Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

144 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BunMail

CI CodeQL License: MIT Bun Elysia Drizzle ORM PostgreSQL

Send transactional email from your own server. No subscriptions. No per-email pricing. No vendor lock-in.

A self-hosted alternative to SendGrid, Resend, and Postmark — built in TypeScript on Bun + Elysia, with a REST API, web dashboard, DKIM signing, an email queue with retries, webhooks, templates, inbound SMTP, and Gmail-style trash. Ships in a single Docker container.

Why BunMail?

BunMail SendGrid / Resend Postal Mailcow
Self-hosted
Free ✅ MIT $20/mo+ ✅ MIT
API-first ❌ (mail hosting)
One container n/a ❌ (multiple services) ❌ (heavy stack)
Modern stack Bun + Elysia + Drizzle n/a Ruby + RabbitMQ PHP + many services
Web dashboard
Templates
Inbound SMTP $$

BunMail targets developers who want a programmatic transactional email API and nothing else — Postal is heavier, Mailcow targets full mail-server hosting with end-user webmail, and the SaaS options charge per email.

Quick Start (60 seconds)

git clone https://github.com/mohamedboukari/bunmail.git && cd bunmail
cp .env.example .env
echo "POSTGRES_PASSWORD=$(openssl rand -hex 16)" >> .env
docker compose up -d

Then seed your first API key and send a test email:

docker compose exec app bun run src/db/seed.ts
# copy the bm_live_... key it prints

curl -X POST http://localhost:3000/api/v1/emails/send \
  -H "Authorization: Bearer bm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "to": "user@example.com",
    "subject": "Hello from BunMail",
    "html": "<p>It works.</p>"
  }'

Open the dashboard at http://localhost:3000/dashboard (set DASHBOARD_PASSWORD in .env to enable).

Local dev without Docker? See docs/self-hosting.md.

Will my mail land in inbox?

Honest answer: it depends on your sending setup, not on BunMail. Self-hosted transactional email is a reputation problem, not a code problem.

  • You'll land in inbox if you send from a domain with a clean reputation, your server's IP isn't on a blacklist, and you've published correct SPF / DKIM / DMARC records (BunMail handles SPF/DKIM/DMARC for you — see docs/self-hosting.md).
  • ⚠️ You'll likely land in spam if you send from a brand-new .xyz/.top-style domain on a fresh budget VPS IP. That's not a BunMail bug — Gmail and Outlook penalise both factors heavily for any sender.
  • 🛠️ Workarounds: warm up your domain over 2–3 weeks (low volume, real recipients), or plug in a reputable SMTP relay (Postmark, SES, Resend) as the actual sender while keeping BunMail's queue, dashboard, and templates. Relay mode is on the roadmap.

Run mail-tester.com to get a deliverability score for your specific setup before deploying.

Screenshots

Dashboard

Send Email Emails list
Inbound emails Templates
Domains API Keys

Features

  • Direct SMTP delivery — sends straight to recipient MX servers, no relay needed
  • SMTP submission — apps (Infisical, Netbird, Dify, any Nodemailer backend) send through BunMail over SMTP using an API key as the password; switch an app off SendGrid/Brevo with a credentials-only change (docs/smtp-submission.md)
  • DKIM signing — auto-generates 2048-bit RSA keys per domain
  • SPF / DKIM / DMARC verification — DNS checks built into the dashboard
  • Email queue — Postgres-backed with 3 retries, crash recovery, and exactly-once delivery semantics
  • Templates — Mustache-style {{variable}} substitution
  • Webhooks — HMAC-signed events with timestamp-bound replay protection: email.sent, email.failed, email.bounced, email.received, email.complained
  • Inbound SMTP — receive and store incoming mail with DNSBL, recipient validation, and per-IP rate limiting
  • Inbound notifications — per-domain "you have new mail" summary email on receive, DKIM-signed from the recipient domain; opt-in via each domain's notify_email
  • API key auth — SHA-256 hashed Bearer tokens with sliding-window rate limiting
  • Trash + auto-purge — Gmail-style soft delete on outbound and inbound, restorable until purged after TRASH_RETENTION_DAYS (default 7)
  • Suppression list — per-API-key list of addresses we refuse to send to; gate runs at POST /emails/send so bounced/unsubscribed recipients can't re-tank IP reputation
  • DMARC aggregate report ingest — daily rua reports from Microsoft / Google / Yahoo are auto-parsed (gzip / zip / raw XML) and surfaced in the dashboard so misaligned source IPs are visible at a glance
  • Web dashboard — server-rendered (Elysia JSX), bulk operations, real-time stats (24h sent, success rate, queue depth)
  • OpenAPI 3.0 — interactive docs at /api/docs
  • Type-safe — strict TypeScript, Drizzle ORM, no any

API Endpoints

All endpoints (except /health) require Authorization: Bearer <api-key>. Full reference: docs/api.md.

Emails

Method Path Description
POST /api/v1/emails/send Queue an email (direct or template)
GET /api/v1/emails List emails (excludes trash)
GET /api/v1/emails/trash List trashed emails
GET /api/v1/emails/:id Get email by ID
DELETE /api/v1/emails/:id Move to trash
POST /api/v1/emails/bulk-delete Bulk move to trash
POST /api/v1/emails/:id/restore Restore from trash
DELETE /api/v1/emails/:id/permanent Permanently delete a trashed email
POST /api/v1/emails/trash/empty Empty trash

Inbound

Method Path Description
GET /api/v1/inbound List received emails
GET /api/v1/inbound/trash List trashed inbound
GET /api/v1/inbound/:id Get inbound email by ID
DELETE /api/v1/inbound/:id Move to trash
POST /api/v1/inbound/bulk-delete Bulk move to trash
POST /api/v1/inbound/:id/restore Restore from trash
DELETE /api/v1/inbound/:id/permanent Permanently delete a trashed inbound
POST /api/v1/inbound/trash/empty Empty inbound trash

Suppressions

Method Path Description
POST /api/v1/suppressions Add an address to the suppression list (idempotent)
GET /api/v1/suppressions List suppressions (paginated, optional ?email= filter)
GET /api/v1/suppressions/:id Get a suppression by ID
DELETE /api/v1/suppressions/:id Remove (recipient becomes eligible immediately)

Sends to suppressed recipients return 422 with code: "RECIPIENT_SUPPRESSED" and suppressionId. See docs/suppressions.md and docs/bounces.md for the auto-suppression flow on hard bounces.

DMARC Reports

Method Path Description
GET /api/v1/dmarc-reports List DMARC aggregate reports (paginated, optional ?domain=)
GET /api/v1/dmarc-reports/:id Get a single report with its per-source-IP records

DMARC rua aggregate reports that arrive at BunMail's inbound SMTP are auto-detected, decompressed (gzip / zip / raw XML), parsed, and stored. The dashboard at /dashboard/dmarc-reports highlights misaligned source IPs in amber. See docs/dmarc-reports.md.

Domains, Templates, API Keys, Webhooks

See docs/api.md for full coverage of /api/v1/domains, /api/v1/templates, /api/v1/api-keys, /api/v1/webhooks.

Send with a Template

# Create a template
curl -X POST http://localhost:3000/api/v1/templates \
  -H "Authorization: Bearer $BM_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Welcome",
    "subject": "Welcome, {{name}}!",
    "html": "<h1>Hi {{name}}</h1><p>Welcome to {{company}}.</p>",
    "variables": ["name", "company"]
  }'

# Send using it
curl -X POST http://localhost:3000/api/v1/emails/send \
  -H "Authorization: Bearer $BM_KEY" -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "to": "user@example.com",
    "templateId": "tpl_YOUR_TEMPLATE_ID",
    "variables": { "name": "Alice", "company": "Acme Inc" }
  }'

Tech Stack

Layer Technology
Runtime Bun
Framework Elysia
ORM Drizzle ORM (drizzle-orm/bun-sql)
Database PostgreSQL 16+
SMTP outbound Nodemailer (direct mode + DKIM)
SMTP inbound smtp-server + mailparser
Dashboard Elysia JSX (@elysiajs/html + @kitajs/html)
Deployment Docker + Docker Compose

Architecture

HTTP (REST API + dashboard)  ─────► Elysia plugins (modules/)
                                          │
                                          ▼
                                    Services layer
                                          │
                            ┌─────────────┼─────────────┐
                            ▼             ▼             ▼
                       PostgreSQL    Email queue   Webhook dispatch
                                       (poll)        (HMAC-signed)
                                          │
                                          ▼
                              Nodemailer + DKIM signing
                                          │
                                          ▼
                              Recipient MX servers (port 25)

Inbound mail arrives via the built-in SMTP server (port 25/2525), gets parsed by mailparser, and is stored in inbound_emails. Webhooks fire email.received on inbound and email.sent / email.failed on outbound. See ARCHITECTURE.md for full diagrams.

Development

bun install
bun run dev            # start dev server (--watch)
bun test               # run all tests
bun test test/unit     # unit tests only
bun test test/e2e      # integration tests only
bunx tsc --noEmit      # type check
bun run lint           # eslint
docker compose up -d   # full stack with Postgres

See docs/ for module-level documentation:

Deliverability Setup

For inbox placement on Gmail / Outlook / Yahoo, you need:

  1. A clean sender domain — older, no spam history.
  2. A clean IP — check at mxtoolbox.com/blacklists.
  3. PTR (reverse DNS) matching MAIL_HOSTNAME in .env.
  4. SPF, DKIM, DMARC records — BunMail's dashboard tells you exactly what to publish per domain.
  5. Patience — fresh domain + IP combos take 2–3 weeks of low-volume sending to build reputation.

See docs/self-hosting.md for the exact DNS record values.

Roadmap

Active issues: github.com/mohamedboukari/bunmail/issues

Near-term highlights:

  • Webhook delivery persistence + replay (in-memory retries today)
  • Trash purge tombstones for audit trail
  • Optional SMTP relay mode (use Resend / SES / Postmark as the underlying sender)
  • Bun-native SMTP client (subsumes per-domain TLS observability and requireValidCert)

Contributing

Issues, PRs, and discussions welcome. See CONTRIBUTING.md and the good first issue label for ways to start.

License

MIT

Releases

Packages

Used by

Contributors

Languages