maily
Self-hosted multi-provider temporary email API with a sleek admin dashboard & modern async Python client.
Create disposable mailboxes on demand, receive emails in real time, extract verification codes and confirmation links automatically — all through one clean REST API, an async-first Python client, and a dark-themed web console. Runs locally or in Docker, stores state in SQLite, and protects access with scoped API keys and encrypted credentials.
✨ Features
- 8 Supported Providers — Aggregates public temporary-mail networks and private Gmail accounts:
mail.tm— High reliability, custom usernames, passwords, full MIME sourcetempmail.plus— Custom usernames, PIN protection, domains, MIME sourcetemp-mail.io— Disposable web mailboxes with automatic CORS tokensnoopmail.org— Domain rotation and raw source inspectionanonymmail.net— Session & CSRF-managed disposable maildispoemail.org— Astro-session based disposable mailtemp.tf— Real provider aliases on Outlook, Hotmail, Gmail, and high.edu.pl with plus/dot syntaxgmail— OAuth2 integration with unlimitedusername+alias@gmail.commailboxes
- Magic Verification Extraction — Wait for an incoming email and automatically extract 4–8 digit verification codes (e.g.
849201) or confirmation action links with a single async call. - Async Python Client — Zero-dependency standalone client (
httpx,pydantic,loguru) with async context managers, async iterators, typed pydantic models, and granular exceptions. - Real-Time Streaming — Stream incoming messages via
async for message in api.messages.iter_messages(box): .... - Admin Dashboard & Developer Hub — Modern dark-mode UI with live charts, message preview with verification highlights, auto-refresh polling, and an integrated Swagger / API documentation explorer at
/docs. - Scoped API Keys — Root keys and granular issued keys with provider/action/parent restrictions and custom TTL cleanup rules.
- Automatic Retention & DB Maintenance — Configurable background worker sweeps expired mailboxes and messages based on TTL policies, paired with automatic throttled database maintenance (WAL truncate, VACUUM, REINDEX, ANALYZE, PRAGMA optimize, quick_check) to defragment storage and optimize query performance without downtime.
- Domain Cache — Periodic background cache of provider domains with instant refresh.
- Docker-Ready — Hermetic multi-stage Docker build with SQLite volume persistence.
🌐 Supported Providers & Capabilities
| Provider | Target Kind | Custom Username | Custom Domain | Passwords / PIN | Raw Source | Read Tracking |
|---|---|---|---|---|---|---|
| mail.tm | domain |
✅ Supported | ✅ Supported | ✅ Password | ✅ 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 |
💡 Extending Providers: Want to add your own provider? Check out the comprehensive guide for developers and AI agents in docs/ADDING_PROVIDERS.md.
🌐 Live Provider Operational Status
Last Verified:
2026-11-02 02:22 UTC| Overall Health: 🟡 Degraded Performance
| Provider | Slug | Targets | API Health | E2E Delivery | Latency | Status |
|---|---|---|---|---|---|---|
| Mail.tm | mail.tm |
1 | 🟢 PASS | 🟢 6.8s |
1.53s |
🟢 Operational |
| TempMail Plus | tempmail.plus |
8 | 🟢 PASS | 🟢 3.4s |
1.19s |
🟢 Operational |
| TempMail.io | temp-mail.io |
6 | 🟢 PASS | 🟢 3.6s |
1.09s |
🟢 Operational |
| Temp.tf | temp.tf |
4 | 🟢 PASS | 🟢 7.9s |
0.84s |
🟢 Operational |
| AnonymMail.net | anonymmail.net |
6 | 🟢 PASS | 🟢 5.4s |
1.78s |
🟢 Operational |
| DispoEmail.org | dispoemail.org |
4 | 🟢 PASS | 🟢 3.2s |
0.27s |
🟢 Operational |
| NoopMail.org | noopmail.org |
— | 🔴 FAIL | — | 30.00s |
🔴 API Outage |
Automated health checks run twice daily via GitHub Actions.
🚀 Quick Start
Option A — Docker (recommended)
git clone https://github.com/stormsia/maily.git && cd maily
cp .env.example .env # adjust if needed
docker compose up -d --build
- Web Dashboard: http://127.0.0.1:8477/
- API Documentation: http://127.0.0.1:8477/docs
Option B — Local (Python 3.13+)
1. From PyPI (Recommended)
pip install maily-server
maily
2. From Git Source (Development)
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
Development server with auto-reload:
uvicorn maily.app.main:create_app --factory --reload --port 8477
🐍 Standalone Python Client SDK (maily-client)
The repository includes a dedicated, self-contained standalone Python client package (maily-client) requiring only httpx, pydantic, and loguru. It supports both high-concurrency async/await (Maily) and a synchronous facade (SyncMaily).
📖 Full Documentation: See docs/CLIENT_GUIDE.md and client/README.md for comprehensive recipes (Pytest fixtures, Playwright/Selenium automation, streaming, and retry policies).
Installation
Preferred — PyPI package:
pip install maily-client
Alternative options:
# 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 into your project:
curl -O https://raw.githubusercontent.com/stormsia/maily/main/client/client.py
⚠️ Important: Attribute Access vs Dictionary Key Access
In
maily-client, native dot/attribute access is supported ONLY on Pydantic models and specificallyExtractedConfirmation(which implementsMappingand supports dual access: e.g.res.subject,res.confirmation_codeas well asres["confirmation_link"]).All other client methods returning standard Python dictionaries (such as
await api.config.get()) DO NOT support attribute/dot access. You MUST use standard dictionary key indexing (dict["key"]), never dot access (dict.key).
1-Line Mailbox & Verification Code Extraction
import asyncio
from maily.client import Maily, WaitTimeoutError
async def main():
async with Maily("http://127.0.0.1:8477", api_key="sk-...") 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())
Confirmation Extraction (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']}")
Synchronous Client (SyncMaily)
from maily.client import SyncMaily
with SyncMaily("http://127.0.0.1:8477", api_key="sk-...") as api:
box = api.create_temp_mailbox()
code = api.wait_for_code(box, timeout=45.0)
print(f"Received code synchronously: {code}")
Confirmation Link Extraction
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}")
Real-Time Email Streaming (Async Iterator)
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)
Granular Exception Handling
All API errors inherit from ApiError, which inherits from ClientError:
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!")
Scoped API Key Management
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}")
Retention & Database Maintenance
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)
🖥️ Web Dashboard & Documentation Hub
Access the admin console at http://127.0.0.1:8477/:
- Overview — Live metrics, incoming message 7-day trend, active providers, and manual retention sweep.
- Mailboxes — Instant search, filter by provider, 1-click address copy, and creation dialog with provider capability awareness.
- Message Reader — Reading pane with automatic Quick Extract card (highlighting 6-digit codes and action links with 1-click copy), formatted HTML / Raw HTML tabs, RFC822 source viewer, and user-controllable auto-refresh interval (5s / 10s / 30s / Off).
- API Keys — Create and manage scoped keys, reveal secrets, and test the dashboard under a specific key identity.
- Retention & Database Maintenance — Real-time cache status, scheduled refresh intervals, TTL cleanups, and automated database maintenance (VACUUM, REINDEX, ANALYZE).
- Developer Hub (
/docs) — Multi-tab developer portal featuring interactive Swagger UI with automatic API key preauthorization, Python client snippets, provider status matrix, and dynamic cURL commands.
🛡️ Architecture & CI/CD Pipeline
The project implements a state-of-the-art continuous integration and delivery architecture with multi-layer security scanning:
flowchart LR
subgraph CI["Fast Feedback CI (< 90s)"]
lint["Ruff + HTMLHint"]
alembic["Alembic Migrations Check"]
deptry["Deptry Clean Dependencies"]
mypy["Mypy Strict Type Check"]
pytest["Pytest Matrix (3.13-3.15) + Coverage >= 80%"]
smoke["Docker Runtime Smoke Test"]
end
subgraph Security["Security & SAST"]
codeql["CodeQL SAST (Python + JS)"]
gitleaks["Gitleaks Secret Scanning"]
pipaudit["pip-audit PyPA Advisory CVE"]
trivy["Trivy Container & OS Scan"]
end
subgraph CD["Release & Distribution"]
pypi["PyPI Trusted Publisher (OIDC)"]
ghcr["Multi-Arch GHCR (amd64/arm64)"]
sbom["SPDX Signed SBOM"]
pages["GitHub Pages Interactive Docs"]
end
CI --> Security --> CD
Security & Compliance Controls
- GitHub CodeQL Analysis — Deep semantic static application security testing (SAST) detecting SQL injection, path traversal, and SSRF.
- Gitleaks Secret Scanning — Pre-merge blocking of accidental token, key, or credential commits (
.gitleaks.tomlconfigured). - Trivy Container Scanner — Full CVE inspection of Debian Bookworm OS packages and Python wheels exported directly to the GitHub Security tab (SARIF).
- pip-audit — Automated vulnerability audit against the PyPA Advisory Database.
- Hermetic Test Suite — 360+ tests passing hermetically with 80%+ code coverage across Python 3.13, 3.14, and 3.15.
⚙️ Configuration Reference
Copy .env.example to .env and customize:
| Variable | Default | Description |
|---|---|---|
MAILY_HOST |
127.0.0.1 |
Bind address (0.0.0.0 in Docker) |
MAILY_PORT |
8477 |
HTTP port |
MAILY_API_KEY |
(empty) | Root master key. If empty on loopback, open root access is allowed. Non-loopback requires a key. |
MAILY_DATABASE_URL |
sqlite+aiosqlite:///mailtm.db |
SQLite database URI |
MAILY_SECRET_KEY |
(auto-generated) | Fernet encryption key used to protect credentials |
MAILY_PROXY |
(empty) | Optional HTTP/SOCKS5 proxy for upstream provider requests |
MAILY_RETENTION_INTERVAL_SECONDS |
60 |
Background retention sweep frequency |
MAILY_DB_MAINTENANCE_ENABLED |
true |
Enable periodic database maintenance during retention |
MAILY_DB_MAINTENANCE_INTERVAL_MINUTES |
60 |
Minimum interval between database maintenance runs (in minutes) |
MAILY_SQLITE_BUSY_TIMEOUT_MS |
60000 |
SQLite lock wait timeout in ms to prevent request drops during maintenance |
MAILY_DOMAIN_CACHE_INTERVAL_SECONDS |
1800 |
Background domain refresh frequency |
MAILY_GMAIL_CLIENT_ID |
(empty) | Google OAuth2 Client ID |
MAILY_GMAIL_CLIENT_SECRET |
(empty) | Google OAuth2 Client Secret |
MAILY_TEMP_TF_GMAIL_DOT |
true |
Enable dot-aliased Gmail addresses for temp.tf (user.name@gmail.com) |
MAILY_TEMP_TF_GMAIL_PLUS |
true |
Enable plus-aliased Gmail addresses for temp.tf (username+tag@gmail.com) |
🧪 Testing & Code Quality
The repository has 100% passing tests and strict typing and linting checks:
# Run full test suite across all workers
python -m pytest
# Run ruff linter & formatter checks
uv run ruff check src tests client scripts
# Run mypy static type checking
uv run mypy src client scripts
# Check Alembic migrations consistency
uv run alembic upgrade head
uv run alembic check
# Verify clean pyproject.toml dependencies
uvx deptry src
# Verify standalone client mirror integrity
python scripts/build_client.py --check
📄 License
MIT © stormsia
Metadata
Release files for maily-server 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| maily_server-0.4.0.tar.gz | 517.8 kB | With provenance | Details for maily_server-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| maily_server-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for maily_server-0.4.0-py3-none-any.whl |
Total release size: 717.7 kB
Release files / maily_server-0.4.0.tar.gz
| Download URL | maily_server-0.4.0.tar.gz |
|---|---|
| Size | 517.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
95a1768ce300a9c4fb49d1d20edc367bbe25cb8ffa7256968c1d2d481c0b24e7
|
|
BLAKE2b-256 checksum How to use checksums |
43c8dc87058c63ae4d152afb98f023bb82ad9d2da5a407813eba1bd8a9a96db9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / maily_server-0.4.0-py3-none-any.whl
| Download URL | maily_server-0.4.0-py3-none-any.whl |
|---|---|
| Size | 199.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
50e50df969ac4a5a4df73e7d4b40b856491bf720ef34131a81dce4ddcdcf198e
|
|
BLAKE2b-256 checksum How to use checksums |
d8f5e6aeb074a55f58f031124ad57c560a7d99ca308462c42acc08c74ee901b1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for maily-server 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for maily_server-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.