Metadata-Version: 2.4
Name: mailfixture
Version: 0.6.0
Summary: Email, SMS, and webhook fixtures for test suites — programmatic inboxes, capture hooks, long-polling, and OTP extraction.
Project-URL: Homepage, https://mailfixture.com
Project-URL: Documentation, https://mailfixture.com/docs
Author-email: MailFixture <support@mailfixture.com>
License-Expression: MIT
License-File: LICENSE
Keywords: e2e,email,fixtures,otp,playwright,pytest,sms,testing,webhook,webhook-capture,webhook-testing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# mailfixture

Email fixtures for your test suite. Create a programmatic inbox in one call,
point your app's signup / OTP / password-reset flow at it, then wait for the
email — parsed, with the OTP and links already extracted.

Receive-only, built for CI: long-polling instead of retry loops, zero
dependencies outside the Python standard library, Python 3.9+.

```bash
pip install mailfixture
```

## Quickstart

Create a key in [Dashboard → API keys](https://mailfixture.com/app/keys) and
export it as `MAILFIXTURE_API_KEY`.

```python
from mailfixture import MailFixture

mfx = MailFixture()  # reads MAILFIXTURE_API_KEY

def test_signup_sends_otp(page):
    inbox = mfx.create_inbox(ttl_seconds=900)

    page.goto("/signup")
    page.fill("#email", inbox.email_address)
    page.click("text=Send code")

    otp = inbox.wait_for_otp(timeout=30)   # long-polls; no sleep()
    page.fill("#otp", otp)
```

Waiting for a magic link instead:

```python
link = inbox.wait_for_link(kind="verify", timeout=30)
page.goto(link.url)
```

Capturing a webhook your app sends:

```python
hook = mfx.create_hook(label="checkout", ttl_seconds=900)
configure_app_webhook(hook.ingest_url)  # your test setup
trigger_checkout()
request = hook.wait_for_request(match="body:order.created")
assert request.method == "POST"
hook.delete()
```

Everything on the wait helpers is server-side long-polling: the request is
held open until a matching message arrives (or the timeout passes, raising
`mailfixture.MailFixtureTimeout`).

## API surface

| Method | Purpose |
| --- | --- |
| `create_inbox(local_part=, domain=, ttl_seconds=)` | new inbox; returns an `Inbox` handle |
| `inbox.wait_for_message(match=, timeout=)` | first matching message, full body + extraction |
| `inbox.wait_for_otp(match=, timeout=)` | best extracted OTP as a string |
| `inbox.wait_for_link(kind=, match=, timeout=)` | first link, optionally `verify\|reset\|unsubscribe` |
| `inbox.messages(since=, match=, wait=)` / `clear()` / `delete()` | raw listing & lifecycle |
| `get_message(id)`, `get_otp(id)`, `get_links(id)` | direct message access |
| `download_attachment(message_id, index)` | decoded attachment bytes |
| `create_hook(label=, ttl_seconds=)`, `list_hooks()` | webhook capture URL and lifecycle handles |
| `hook.wait_for_request(match=, since=, timeout=)` | first matching captured request, including full headers and body |
| `hook.requests(...)`, `clear()`, `delete()` / `get_hook_request(id)` | capture listing, teardown, and direct detail access |
| `create_domain(fqdn)`, `verify_domain(id)`, `list_domains()`, `delete_domain(id)` | custom domains |
| `create_key(label=)`, `list_keys()`, `revoke_key(id)` | API keys |

`match` filters with `subject:`, `from:`, or `to:` prefixes; a bare term
matches the subject.

Capture-hook matching accepts `method:`, `path:`, and `body:`; a bare term
matches the first 32 KiB of request body bytes. Returned detail bodies are
complete.

Errors are raised as `mailfixture.MailFixtureError` carrying the API's
RFC 7807 `status` / `title` / `detail` fields.

## Development

```bash
PYTHONPATH=src python3 -m unittest discover -s tests
```

Docs: https://mailfixture.com/docs
