Run Maily via official pre-built Docker containers, Docker Compose, or develop locally from source.
docker run -d \
--name maily \
-p 8477:8477 \
-e MAILY_API_KEY=your_secret_api_key \
-v maily_data:/data \
--restart unless-stopped \
ghcr.io/stormsia/maily:latest
# Linux / macOS (curl)
curl -O https://raw.githubusercontent.com/stormsia/maily/main/docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -d
# Windows (PowerShell)
Invoke-WebRequest -Uri https://raw.githubusercontent.com/stormsia/maily/main/docker-compose.prod.yml -OutFile docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -d
# If using Docker Compose:
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
# If using docker run:
docker pull ghcr.io/stormsia/maily:latest
docker stop maily && docker rm maily
docker run -d --name maily -p 8477:8477 -e MAILY_API_KEY=your_secret_api_key -v maily_data:/data --restart unless-stopped ghcr.io/stormsia/maily:latest
git clone https://github.com/stormsia/maily.git && cd maily
cp .env.example .env # adjust if needed
docker compose up -d --build
pip install maily-server
maily
git clone https://github.com/stormsia/maily.git && cd maily
python -m venv .venv
# Windows:
.venv\Scripts\activate
# Linux / macOS:
source .venv/bin/activate
pip install -e ".[dev]"
maily
maily-client is a zero-dependency (only httpx, loguru, pydantic) single-file client.
Full async support, typed pydantic models, granular exception hierarchy, and high-level verification helpers.
pip install maily-client
# Directly from GitHub repository:
pip install "maily-client @ git+https://github.com/stormsia/maily.git#subdirectory=client"
# Or download the zero-dependency single-file client directly:
curl -O https://raw.githubusercontent.com/stormsia/maily/main/client/maily_client.py
import asyncio
# Preferred when using maily-client SDK:
from maily_client import Maily, WaitTimeoutError
# (Or if using the maily-server package directly: from maily import Maily, WaitTimeoutError)
async def main():
async with Maily("http://127.0.0.1:8477", api_key="your_secret_api_key") as api:
# Create a disposable mailbox with automatic domain selection
box = await api.create_temp_mailbox()
print(f"Disposable email: {box.address}")
# Send an email to box.address, then wait for the code:
try:
code = await api.wait_for_code(box, timeout=60.0)
print(f"Received verification code: {code}")
except WaitTimeoutError:
print("Timed out waiting for email.")
asyncio.run(main())
ExtractedConfirmation Dual Access)
async with Maily("http://127.0.0.1:8477") as api:
box = await api.create_temp_mailbox()
# Wait for email and extract both verification code and activation link
res = await api.wait_for_confirmation_email(box, digit_code_mask=r"\b(\d{6})\b")
if res is not None:
# 1. Attribute / dot access:
print(f"Subject: {res.subject}")
print(f"Code: {res.confirmation_code}")
# 2. Key mapping access:
print(f"Link: {res['confirmation_link']}")
print(f"Code: {res['confirmation_code']}")
SyncMaily)
# Standalone client:
from maily_client import SyncMaily
# Or via server package: from maily import SyncMaily
with SyncMaily("http://127.0.0.1:8477", api_key="your_secret_api_key") as api:
box = api.create_temp_mailbox()
code = api.wait_for_code(box, timeout=45.0)
print(f"Received code synchronously: {code}")
async with Maily("http://127.0.0.1:8477") as api:
box = await api.create_temp_mailbox(provider="tempmail.plus")
print(f"Address: {box.address}")
# Automatically extracts URLs containing 'confirm', 'verify', or 'activate'
link = await api.wait_for_link(box, timeout=90.0)
print(f"Confirmation URL: {link}")
async with Maily("http://127.0.0.1:8477") as api:
box = await api.mailboxes.create(domain="mail.tm")
# Continuously yields new messages as they arrive
async for msg in api.messages.iter_messages(box, poll_interval=2.0, timeout=120.0):
print(f"Incoming: {msg.subject} from {msg.from_address}")
detail = await api.messages.get(box, msg.message_api_id)
print("Body text:\n", detail.text)
from maily_client import (
AuthenticationError, # 401: Invalid API key
ForbiddenError, # 403: Scope denied
NotFoundError, # 404: Mailbox or message missing
ValidationError, # 422 / 400: Bad params
RateLimitError, # 429: Rate limited
UpstreamProviderError, # 502/504: Provider downtime
WaitTimeoutError, # Timeout waiting for email/code
)
try:
detail = await api.messages.get(box, "unknown_id")
except NotFoundError:
print("Message not found!")
except AuthenticationError:
print("Check your API key!")
async with Maily("http://127.0.0.1:8477", api_key="root_secret") as api:
# Create key restricted to mail.tm only
new_key = await api.keys.create(
name="bot-worker",
scope={"providers": ["mail.tm"], "parent_ids": [], "actions": ["read", "write"]},
)
print(f"Key Prefix: {new_key.prefix} | Secret: {new_key.secret}")
async with Maily("http://127.0.0.1:8477", api_key="root_secret") as api:
# Run regular retention sweep (triggers DB maintenance if interval has elapsed)
res = await api.admin.retention_run()
print(f"Deleted: {res.mailboxes_deleted} boxes, {res.messages_deleted} messages")
# Or force immediate full database maintenance (VACUUM, REINDEX, ANALYZE):
res = await api.admin.retention_run(force_maintenance=True)
Maily seamlessly aggregates public temporary-mail networks and private Gmail parent accounts into one unified REST API.
| Provider | Target Kind | Custom Username | Custom Domain | Passwords / PIN | Raw Source | Read Tracking |
|---|---|---|---|---|---|---|
| mail.tm | domain | Supported | Supported | Supported | Supported | Supported |
| tempmail.plus | domain | Supported | Supported | PIN Code | Supported | Supported |
| temp-mail.io | domain | Supported | Supported | No | Supported | No-op |
| noopmail.org | domain | Supported | Supported | No | Supported | No-op |
| anonymmail.net | domain | Supported | Supported | No | Supported | No-op |
| dispoemail.org | domain | Supported | Supported | No | Supported | No-op |
| temp.tf | domain | Supported | Supported | No | Supported | No-op |
| gmail | parent | +alias | Own Account | OAuth2 | Supported | Supported |
See the developer guide in docs/ADDING_PROVIDERS.md for a complete step-by-step
walkthrough and copy-pasteable boilerplate for adding any new email service with hermetic unit tests.
Authenticate all requests with the X-API-Key HTTP header.
curl -X GET "${window.location.origin}/v1/targets" \
-H "X-API-Key: ${getSavedKey() || 'YOUR_API_KEY'}" \
-H "Accept: application/json"
curl -X POST "${window.location.origin}/v1/mailboxes" \
-H "X-API-Key: ${getSavedKey() || 'YOUR_API_KEY'}" \
-H "Content-Type: application/json" \
-d '{"provider": "mail.tm", "domain": "mail.tm"}'
curl -X GET "${window.location.origin}/v1/mailboxes/1/messages" \
-H "X-API-Key: ${getSavedKey() || 'YOUR_API_KEY'}"
curl -X GET "${window.location.origin}/v1/mailboxes/1/messages/MESSAGE_ID" \
-H "X-API-Key: ${getSavedKey() || 'YOUR_API_KEY'}"
curl -X POST "${window.location.origin}/v1/keys" \
-H "X-API-Key: ${getSavedKey() || 'YOUR_API_KEY'}" \
-H "Content-Type: application/json" \
-d '{"name": "worker", "scope": {"providers": ["mail.tm"], "actions": ["read", "write"]}}'
curl -X POST "${window.location.origin}/v1/admin/retention/run?force_maintenance=true" \
-H "X-API-Key: ${getSavedKey() || 'YOUR_API_KEY'}"