Skip to main content

Official Python SDK for the MailCapture email testing API

Project description

mailcapture

Official Python SDK for MailCapture — a real email capture API for integration testing OTP codes, verification links, and other transactional emails.

Installation

pip install mailcapture

Requires Python 3.9+.

Quick start

from mailcapture import MailCapture

with MailCapture(api_key) as mc:
    mc.ping()  # validates key, caches your username

    # Send an email to {username}-signup@mailcapture.app, then:
    email = mc.wait_for("signup", timeout=15)

    print(email.subject)   # "Welcome to Acme!"
    print(email.otp)       # "123456" — extracted automatically

The pattern for integration tests

  1. Clear the inbox before each test
  2. Trigger the action that sends the email (register, reset password, etc.)
  3. Wait for the email — wait_for holds the connection open and returns the instant it arrives
  4. Assert on subject, OTP, body, links
  5. Clean up after
# pytest example
import pytest
from mailcapture import MailCapture

@pytest.fixture(scope="session")
def mc():
    with MailCapture(os.environ["MAILCAPTURE_API_KEY"]) as client:
        client.ping()
        yield client

def test_signup_otp(mc):
    inbox = mc.inbox("signup")
    inbox.clear()                             # clean starting state

    register_user(inbox.address)              # "alice-signup@mailcapture.app"

    email = inbox.wait_for(timeout=10)

    assert email.subject == "Verify your email"
    assert re.match(r"^\d{6}$", email.otp)

Async usage

import asyncio
from mailcapture import AsyncMailCapture

async def test_signup():
    async with AsyncMailCapture(api_key) as mc:
        await mc.ping()
        inbox = mc.inbox("signup")
        await inbox.clear()

        await register_user(inbox.address)

        email = await inbox.wait_for(timeout=10)
        assert email.otp is not None

API reference

MailCapture(api_key, *, base_url=..., request_timeout=...)

AsyncMailCapture(api_key, *, base_url=..., request_timeout=...)

Both clients accept the same constructor arguments.

Argument Default Description
api_key required Your mc_... API key
base_url https://mailcapture.app Override for local dev
request_timeout 10.0 Default timeout in seconds

Both support context manager usage (with / async with) for clean connection handling.


ping()PingResult

Validates your API key and returns your address template. Also caches your username so address() works synchronously.

result = mc.ping()
print(result.username)          # "alice"
print(result.address_template)  # "alice-{tag}@mailcapture.app"

wait_for(tag, *, timeout=30, poll_timeout=10, after=None)Capture

Long-polls the API and returns the first email captured for the given tag. The server holds the connection open — no busy-waiting.

email = mc.wait_for("signup", timeout=15)
Argument Default Description
tag required Which inbox to watch
timeout 30 Total wait in seconds
poll_timeout 10 Per-poll server timeout in seconds (max 30)
after 60s ago Only return captures received after this datetime

Raises MailCaptureTimeoutError if no email arrives in time.


inbox(tag)Inbox / AsyncInbox

Returns a scoped inbox object for a tag. Keeps test code clean.

inbox = mc.inbox("password-reset")

inbox.address          # "alice-password-reset@mailcapture.app" (requires ping() first)
inbox.wait_for(timeout=10)
inbox.list(limit=5)
inbox.clear()

address(tag)str

Generates the capture email address synchronously. Requires ping() first.

mc.ping()
mc.address("signup")  # "alice-signup@mailcapture.app"

list(*, tag=None, after=None, limit=None)CaptureList

List recent captures (newest first).

result = mc.list(tag="signup", limit=10)
for email in result.items:
    print(email.subject)

get(capture_id)Capture

Get a single capture by ID. Raises MailCaptureNotFoundError if not found.


delete(tag)None

Delete all captures for a tag. Use before each test for a clean inbox.


The Capture object

@dataclass
class Capture:
    id: str           # UUID
    tag: str          # e.g. "signup"
    subject: str      # email subject line
    otp: str | None   # extracted OTP/code, if detected
    body_text: str | None
    body_html: str | None
    latency_ms: int   # time from send to capture, in ms
    status: str
    received_at: str  # ISO 8601 timestamp

The otp field is extracted automatically. If your OTP is embedded in a sentence, the service finds it for you. None if no code was detected.


Error handling

All errors extend MailCaptureError and have a .code attribute.

from mailcapture import (
    MailCaptureAuthError,
    MailCaptureTimeoutError,
    MailCaptureNotFoundError,
    MailCaptureNetworkError,
)

try:
    email = mc.wait_for("signup", timeout=10)
except MailCaptureTimeoutError as e:
    print(f"Waited {e.waited_seconds:.0f}s for tag '{e.tag}' — nothing arrived")
    print("Did the email actually send? Check your email service logs.")
except MailCaptureAuthError:
    print("Check your MAILCAPTURE_API_KEY environment variable.")
except MailCaptureNetworkError:
    print("Could not reach MailCapture. Check your network connection.")
Exception .code When
MailCaptureAuthError UNAUTHORIZED Invalid or revoked API key
MailCaptureTimeoutError TIMEOUT wait_for exceeded its timeout
MailCaptureNotFoundError NOT_FOUND get(id) — capture not found
MailCaptureNetworkError NETWORK_ERROR Could not reach the API
MailCaptureApiError varies Unexpected API error

Parallel tests

Each tag is its own inbox — safe to run concurrently.

import asyncio
from mailcapture import AsyncMailCapture

async def test_parallel():
    async with AsyncMailCapture(api_key) as mc:
        await mc.ping()

        signup = mc.inbox("signup")
        reset  = mc.inbox("password-reset")

        await asyncio.gather(signup.clear(), reset.clear())

        # Trigger both emails...

        signup_email, reset_email = await asyncio.gather(
            signup.wait_for(timeout=15),
            reset.wait_for(timeout=15),
        )

Local development

mc = MailCapture(api_key, base_url="http://localhost:3002")

Environment variable

The SDK does not read environment variables automatically. Pass your key explicitly:

import os
mc = MailCapture(os.environ["MAILCAPTURE_API_KEY"])

Get your API key at mailcapture.app/admin/api-keys.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mailcapture-1.0.0.tar.gz (18.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mailcapture-1.0.0-py3-none-any.whl (20.3 kB view details)

Uploaded Python 3

File details

Details for the file mailcapture-1.0.0.tar.gz.

File metadata

  • Download URL: mailcapture-1.0.0.tar.gz
  • Upload date:
  • Size: 18.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for mailcapture-1.0.0.tar.gz
Algorithm Hash digest
SHA256 050492562f7d1a2faecc408ed6042121bd1edd8ec19f598da2c3afc66d899c72
MD5 4cd13f077e6d70b979cc1a3020c7d6db
BLAKE2b-256 174cf568446cade76a4c1591328a7f056d30c0881687075202501257a5790dfb

See more details on using hashes here.

File details

Details for the file mailcapture-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: mailcapture-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 20.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for mailcapture-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d748019d9f654906a135b3ca7642c9eaf6b9d631916a777d71db0580e73c7854
MD5 e995a10efc0c19d7a8a99bb03c85e627
BLAKE2b-256 9c86b91a308c32a51ccdaca138d68d38894668ef8ab8152acf062ecedb3f89e9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page