Skip to main content

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.

CI Pipeline CodeQL Security Security Audit Gitleaks Scan Code Coverage Docker GHCR PyPI server PyPI client API Docs Python License: MIT


✨ Features

  • 8 Supported Providers — Aggregates public temporary-mail networks and private Gmail accounts:
    • mail.tm — High reliability, custom usernames, passwords, full MIME source
    • tempmail.plus — Custom usernames, PIN protection, domains, MIME source
    • temp-mail.io — Disposable web mailboxes with automatic CORS tokens
    • noopmail.org — Domain rotation and raw source inspection
    • anonymmail.net — Session & CSRF-managed disposable mail
    • dispoemail.org — Astro-session based disposable mail
    • temp.tf — Real provider aliases on Outlook, Hotmail, Gmail, and high.edu.pl with plus/dot syntax
    • gmail — OAuth2 integration with unlimited username+alias@gmail.com mailboxes
  • 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

git clone https://github.com/stormsia/maily.git && cd maily
cp .env.example .env        # adjust if needed
docker compose up -d --build

Option B — Local (Python 3.13+)

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 specifically ExtractedConfirmation (which implements Mapping and supports dual access: e.g. res.subject, res.confirmation_code as well as res["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}")
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

  1. GitHub CodeQL Analysis — Deep semantic static application security testing (SAST) detecting SQL injection, path traversal, and SSRF.
  2. Gitleaks Secret Scanning — Pre-merge blocking of accidental token, key, or credential commits (.gitleaks.toml configured).
  3. Trivy Container Scanner — Full CVE inspection of Debian Bookworm OS packages and Python wheels exported directly to the GitHub Security tab (SARIF).
  4. pip-audit — Automated vulnerability audit against the PyPA Advisory Database.
  5. 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)

Source distribution for maily-server 0.4.0
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)

Table of built distributions (wheels) for maily-server 0.4.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page