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
- Clear the inbox before each test
- Trigger the action that sends the email (register, reset password, etc.)
- Wait for the email —
wait_forholds the connection open and returns the instant it arrives - Assert on subject, OTP, body, links
- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mailcapture-1.0.1.tar.gz.
File metadata
- Download URL: mailcapture-1.0.1.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40a382b55899178254ed927ef2eb63e3b5d6da90e8cdf3fbaab9d63be827f861
|
|
| MD5 |
9ca257138d8d51c2799b5bb073fa90fa
|
|
| BLAKE2b-256 |
e1717e174610d9dbf2e1a93bc1a567f98eea0b5b2cebb8e67bae11a45e47126e
|
File details
Details for the file mailcapture-1.0.1-py3-none-any.whl.
File metadata
- Download URL: mailcapture-1.0.1-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9cfc444e475790d635e93baf3cd78d22d809d6a248f06a8d393f8f6ff4bdda6a
|
|
| MD5 |
497f16aba697b4ee47aa36c68814c794
|
|
| BLAKE2b-256 |
15a9bb421b6b111910944a3e47143a0a177126b74be6543454ac1386a556cfe1
|