Free · Self-hosted · Edge-native

jmail Documentation

A free, self-hosted disposable email service that runs entirely on Cloudflare Workers. Receive email, store it in D1, and serve a clean UI from the edge.

1Getting Started

Deploy your own instance in five steps. You need a Cloudflare account and a domain with nameservers pointed at Cloudflare.

Prerequisites

  • Cloudflare account (free tier is enough)
  • A domain added to Cloudflare
  • Node.js v18+ and npm

1. Clone and install

git clone https://github.com/josskixg/jmail.git
cd jmail
npm install

2. Login to Cloudflare

npx wrangler login

3. Configure wrangler.toml

Set your mail domains, web host, branding, and theme variables. See the Configuration section for the full list.

4. Create the D1 database

npx wrangler d1 create jmail-db

Copy the returned database_id into wrangler.toml, then apply the schema:

npx wrangler d1 execute jmail-db --remote --file=src/db/schema.sql

5. Deploy

npx wrangler deploy
Next: enable Email Routing on your domain so inbound messages reach the Worker.

2Configuration

All settings live in wrangler.toml under [vars]. Environment variables are read at runtime; redeploy after changes.

App settings

VariableTypeDefaultDescription
APP_NAMEstringjmailDisplay name shown in the UI
MAIL_DOMAINstringatiku.web.idComma-separated list of allowed mail domains
WEB_HOSTstringmail.atiku.web.idWeb frontend hostname
TAGLINEstringFree Disposable Temporary Email & Anonymous InboxTagline below the app name
FOOTER_TEXTstringCREATED BY JMailFooter attribution text
META_KEYWORDSstringjmail, temp mail, ...Content for <meta name="keywords">
META_AUTHORstringJMAILContent for <meta name="author">

Ads variables

VariableTypeDefaultDescription
ADS_ENABLEDstringfalseSet true to activate ad slots
ADS_CLIENTstringAdSense publisher ID (e.g. ca-pub-1234567890)
ADS_SLOT_HEADERstringAdSense slot ID for header placement
ADS_SLOT_INLISTstringAdSense slot ID between inbox rows
ADS_SLOT_FOOTERstringAdSense slot ID for footer placement
ADS_CUSTOM_HEADERstringCustom HTML/JS for header ad, overrides AdSense slot
ADS_CUSTOM_INLISTstringCustom HTML/JS for in-list ad, overrides AdSense slot
ADS_CUSTOM_FOOTERstringCustom HTML/JS for footer ad, overrides AdSense slot

Theme variables

Leave empty to keep the default theme. Each value maps to a CSS custom property.

VariableCSS PropertyControls
THEME_BG--bg-canvasBackground color
THEME_PRIMARY--pink-flowerPrimary brand color
THEME_PRIMARY_SOFT--pink-softPrimary lighter shade
THEME_ACCENT--yellow-flowerAccent / secondary color
THEME_ACCENT_SOFT--yellow-softAccent lighter shade
Tip: set soft shades with alpha for a subtle tint, e.g. #2563eb33.

3API Reference

All endpoints live under /api/. Base URL: https://mail.atiku.web.id/api/

Sessions are anonymous. Pass an optional x-session-id header to group and persist inboxes in the browser.

App configuration

GET /api/config Public app config: branding, mail domains, ads, theme.
curl -s https://mail.atiku.web.id/api/config | jq

Response 200 OK:

{
  "appName": "jmail",
  "mailDomain": "atiku.web.id",
  "mailDomains": ["atiku.web.id", "lalekne.web.id"],
  "webHost": "mail.atiku.web.id",
  "tagline": "Free Disposable Temporary Email & Anonymous Inbox",
  "footerText": "CREATED BY JMail",
  "ads": {
    "enabled": false,
    "client": "",
    "slots": { "header": "", "inlist": "", "footer": "" },
    "custom": { "header": "", "inlist": "", "footer": "" }
  },
  "theme": {}
}

theme contains any CSS variable overrides (e.g. {"--bg-canvas": "#0a0a0f"}). ads only lists enabled slots.

Create or retrieve a session

GET /api/session Omit x-session-id to create a new session, or pass one to reuse it.
# New session
curl -s https://mail.atiku.web.id/api/session

# Reuse an existing session
curl -s https://mail.atiku.web.id/api/session \
  -H "x-session-id: 550e8400-e29b-41d4-a716-446655440000"

Response 200 OK: {"sessionId": "550e8400-e29b-41d4-a716-446655440000"}

List inboxes

GET /api/inboxes Lists all inboxes linked to your session.
curl -s https://mail.atiku.web.id/api/inboxes \
  -H "x-session-id: 550e8400-e29b-41d4-a716-446655440000" | jq

Response 200 OK:

[
  { "address": "kopihujan23@atiku.web.id", "created_at": "2026-06-26 07:48:19" }
]

Errors: 400 Missing x-session-id

Create an inbox

POST /api/inboxes Creates a new inbox or claims an existing address, linked to your session.
# Random address
curl -s -X POST https://mail.atiku.web.id/api/inboxes \
  -H "x-session-id: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{}'

# Custom local-part on a specific domain
curl -s -X POST https://mail.atiku.web.id/api/inboxes \
  -H "x-session-id: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"localPart": "test", "domain": "lalekne.web.id"}'

Response 201 Created:

{ "address": "langitbiru23@atiku.web.id", "created_at": "2026-06-26 07:48:19" }

Errors: 400 Missing x-session-id, 400 Invalid domain: ... (domain must be in mailDomains)

Delete an inbox from session

DELETE /api/inboxes/:address Removes the inbox link from your session. Does not delete the inbox or its messages.
# Address must be URI-encoded: test123%40atiku.web.id
curl -s -X DELETE "https://mail.atiku.web.id/api/inboxes/test123%40atiku.web.id" \
  -H "x-session-id: 550e8400-e29b-41d4-a716-446655440000"

Response 200 OK: {"ok": true}

Fetch messages

GET /api/inboxes/:address/messages Fetches all messages for an inbox linked to your session.
curl -s "https://mail.atiku.web.id/api/inboxes/test123%40atiku.web.id/messages" \
  -H "x-session-id: 550e8400-e29b-41d4-a716-446655440000" | jq

Response 200 OK:

[
  {
    "id": "msg_1782461413912_0956a83c",
    "inbox_address": "test123@atiku.web.id",
    "from_address": "someone@gmail.com",
    "subject": "Hello",
    "body": "This is the email body",
    "received_at": "2026-06-26 08:10:14"
  }
]

Errors: 400 Missing x-session-id, 403 Inbox not in this session

Public stats

GET /api/stats Public aggregate stats: total inboxes, messages, and configured domains.
curl -s https://mail.atiku.web.id/api/stats | jq

Response 200 OK:

{
  "status": "ok",
  "appName": "jmail",
  "author": "JMAIL",
  "totalInboxes": 128,
  "totalMessages": 543,
  "domains": ["atiku.web.id", "lalekne.web.id"],
  "timestamp": "2026-08-02T00:00:00.000Z"
}

Health check

GET /api/health Liveness probe. Use with uptime monitors.
curl -s https://mail.atiku.web.id/api/health | jq

Response 200 OK:

{
  "status": "healthy",
  "service": "jmail",
  "version": "1.0.0",
  "timestamp": "2026-08-02T00:00:00.000Z"
}

4Theme Customization

jmail's UI is driven by five CSS custom properties. Override any of them with THEME_* env vars in wrangler.toml; empty values keep the default theme.

Cyberpunk

THEME_BG = "#0a0a0f"
THEME_PRIMARY = "#ff2d95"
THEME_PRIMARY_SOFT = "#ff2d9533"
THEME_ACCENT = "#00f0ff"
THEME_ACCENT_SOFT = "#00f0ff33"

Forest

THEME_BG = "#0f1a0f"
THEME_PRIMARY = "#2d8a4e"
THEME_PRIMARY_SOFT = "#2d8a4e33"
THEME_ACCENT = "#c4a35a"
THEME_ACCENT_SOFT = "#c4a35a33"
Note: theme overrides are exposed to the frontend via GET /api/config under theme, so no redeploy of assets is needed, only of the Worker config.

5Ads Setup

Ads are off by default. Two modes: Google AdSense or custom HTML. Three placements: header, in-list, and footer.

AdSense

ADS_ENABLED = "true"
ADS_CLIENT = "ca-pub-1234567890"
ADS_SLOT_HEADER = "1234567890"
ADS_SLOT_INLIST = "1234567891"
ADS_SLOT_FOOTER = "1234567892"

A slot only renders when both ADS_CLIENT and its slot ID are set.

Custom HTML

Any placement can use raw HTML/JS instead. Custom content takes precedence over AdSense for the same slot.

ADS_ENABLED = "true"
ADS_CUSTOM_FOOTER = "<!-- your ad tag --><script>...</script>"
Warning: AdSense reviews require compliant placements. Keep ads unobtrusive and respect Google's policies.

6Email Routing

Inbound email flows through Cloudflare Email Routing. Set it up once per domain and the Worker handles the rest.

1. Enable Email Routing

  1. Cloudflare Dashboard, then your domain, then Email, then Email Routing
  2. Click Get started and enable routing. MX records are created automatically.

2. Route all mail to the Worker

  1. Go to Routing rules, then Catch-all
  2. Set action to Send to a Worker
  3. Select the jmail Worker

3. Verify the Worker binding

In wrangler.toml, the email handler is enabled with:

[email]
action = "process"

4. Add SPF (recommended)

TypeNameContent
TXT@v=spf1 include:_spf.mx.cloudflare.net ~all

Verify

Send a test email to any address at your domain and open it in the web UI. It should appear within seconds (the UI auto-syncs every 5s).

7Deployment

Production deploy

npm run deploy

Equivalent to npx wrangler deploy. Deploys the Worker plus static assets to the edge.

Database migration

npm run db:migrate          # apply schema to production D1
npm run db:local             # apply schema to local D1 (dev)

Local development

npx wrangler dev            # run Worker locally with D1 + assets

Live logs

npx wrangler tail --format pretty

Custom domain

Add a route with custom_domain = true and set workers_dev = false to disable the *.workers.dev URL:

[[routes]]
pattern = "mail.yourdomain.com"
custom_domain = true

Troubleshooting

ProblemFix
Emails not arrivingConfirm the catch-all route targets the Worker and MX records exist
Inbox stays emptyClaim the exact address: New, then type local-part, then Create
DNS_PROBE_FINISHED_NXDOMAINNameservers not pointed at Cloudflare; wait for propagation
Wrangler version warningThis project needs Wrangler v4: npm install --save-dev wrangler@4