Skip to main content

MailFlat — Python SDK

Official Python client for MailFlat: disposable, automation-friendly email inboxes with one-line OTP retrieval. Spin up a real inbox, read the verification code your app just sent, and move on — one call instead of a hand-rolled poll loop, no shared mailbox state.

pip install mailflat

Quickstart

from mailflat import MailFlat

mf = MailFlat(api_key="mf_live_...")  # or set MAILFLAT_API_KEY

# 1 · spin up a disposable inbox
inbox = mf.create(prefix="signup-test", label="checkout flow")
print(inbox.address)            # → signup-test@x7k2m.mailflat.net

# 2 · your app/browser submits the form using inbox.address ...

# 3 · grab the OTP (polls until it arrives or times out)
otp = inbox.wait_for_otp(timeout=30)
print(otp)                      # → "123456"

# 4 · answering a human keeps the same email thread
msg = inbox.wait_for_message(timeout=60)
msg.reply("thanks, received!")

# inbox auto-clears in 2h — no cleanup needed (or call inbox.delete())

For AI agents

Hand an agent one API key and it spins up real inboxes on demand:

mf = MailFlat()  # reads MAILFLAT_API_KEY

inbox = mf.create(prefix="deep-research")
browser.fill("#email", inbox.address)
browser.click("Sign up")

otp = inbox.wait_for_otp(timeout=30)
browser.fill("#code", otp)

API

MailFlat(api_key=None, *, base_url="https://mailflat.net", timeout=30.0, max_retries=2, http_client=None)

Client. api_key falls back to the MAILFLAT_API_KEY environment variable. Use base_url for self-hosted / BYOD deployments. Supports use as a context manager (with MailFlat() as mf:). Pass http_client= an httpx.Client to inject your own transport — this is how you drive the SDK in tests without a network (e.g. httpx.MockTransport).

  • create(*, prefix=None, label=None, subdomain=None, domain=None, retention_hours=None) -> Inbox — open a new inbox. create_inbox(...) is an alias. prefix shapes the address; label does not. prefix="signup-test" gives you signup-test@…, while label is only a name shown in the dashboard so you can tell your inboxes apart. Omitting prefix yields a random agent-… address.
  • list() -> list[Inbox] — inboxes opened with this key.
  • inbox(address) -> Inbox — attach to an existing address without a network call.

Inbox

  • .address — the email address.
  • .messages(*, direction="in") -> list[Message] — messages, newest first.
  • .latest(*, direction="in") -> Message | None — most recent message.
  • .wait_for_otp(*, timeout=30, poll_interval=1.0) -> str — poll until an OTP arrives; returns the code.
  • .wait_for_message(*, timeout=30, poll_interval=1.0, direction="in") -> Message — poll until a message arrives.
  • .send(to, *, subject="", body="", html=None, in_reply_to=None, cc=None, bcc=None, attachments=None) -> dict — send a DKIM-signed email from this inbox. Returns once the mail is accepted for delivery, not once it is delivered (see below). Pass in_reply_to (a Message-ID) to stay in a thread.
  • .message(message_id) -> Message — fetch one message; the way to check what happened to a send.
  • .wait_until_sent(message_id, *, timeout=120, poll_interval=2.0) -> Message — block until delivery finishes. Raises SendFailedError if it permanently failed, SendTimeoutError if it is still queued at the deadline.
  • .mark_read(message_id) -> dict — mark a message read so later polls can skip it.
  • .burn() -> dict — delete every message but keep the address.
  • .download_attachment(message_id, attachment_id) -> bytes — fetch an attachment's bytes.
  • .delete() -> dict — delete the inbox and all its messages.

Reads return received mail by default. direction="out" returns mail you sent from this address, "all" returns both. This matters for agent-to-agent flows: without it, send() followed by wait_for_message() returns your own outgoing message.

Message

.otp, .subject, .sender, .text, .html, .to_address, .direction, .received_at, .links, .attachments, .spam, .headers, .is_read, .message_id, .reply_to_address, .raw.

  • ⚠️ .sender is the SMTP envelope sender (MAIL FROM). On transactional mail that is usually a bounce address such as bounces+abc@sendgrid.net, so send(to=msg.sender, …) delivers your reply to a bounce processor. Use .reply_to_address (or just .reply()), which resolves Reply-ToFrom → envelope sender.

  • .links — URLs found in the body (HTML hrefs first). For "click the verification link" flows.

  • .attachments — metadata; msg.attachments[0].download() fetches the bytes.

  • .spam{score, required, is_spam, rules, scanner}, or None when never scanned (which is not the same as a score of 0). ⚠️ Spam scanning is currently disabled on mailflat.net, so today this is None for every message.

  • .headers — raw headers, exactly as they arrived. Names are case-insensitive per RFC 5322 but this is a plain dict, so do not index it: the real key is Message-ID, and headers["Message-Id"] raises KeyError. Use .header("message-id") or .message_id.

  • .reply(body, *, html=None, subject=None) — answer this message in the same conversation. Fills in the recipient, an Re: subject and the In-Reply-To / References headers Gmail and Outlook thread on. A hand-rolled send() starts a new conversation instead, which does not look like a reply.

  • .mark_read() / .delete() — act on this message directly.

Attachments, cc and bcc

inbox.send(
    "customer@example.com",
    subject="Your invoice",
    body="Attached.",
    cc=["billing@example.com"],
    bcc=["audit@example.com"],
    attachments=["/tmp/invoice.pdf"],          # a path — the SDK reads and encodes it
)

An attachment can be a file path, {"filename": ..., "content": b"..."}, or {"filename": ..., "content_b64": "..."}. Size and count limits depend on your plan (the free plan is deliberately small); going over raises with the limit spelled out rather than silently dropping the file.

bcc recipients receive the mail but never appear in its headers — not even in their own copy. reply() accepts all three too, so you can answer a thread with a file attached.

Did it actually go out?

send() returns as soon as the mail is accepted; delivery runs on a queue, so nothing is delivered yet when it returns:

res = inbox.send("customer@example.com", subject="Your invoice",
                 attachments=["/tmp/invoice.pdf"])
inbox.wait_until_sent(res["message_id"], timeout=120)   # raises if it failed

For anything long-running, subscribe to the message.delivered / message.failed webhooks instead of polling.

Errors

All errors subclass MailFlatError: AuthenticationError (401), MailFlatPermissionError (403, still exported as PermissionError for compatibility — the new name no longer shadows the built-in), NotFoundError (404), RateLimitError (429, carries .retry_after when the server sent one), APIError (other), OTPTimeoutError (no OTP before timeout), EncryptedInboxError (the inbox is end-to-end encrypted, so the server cannot read its contents — use a non-encrypted inbox for agent automation).

License

MIT

Download files

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

Source Distribution

mailflat-0.7.0.tar.gz (21.1 kB view details)

Uploaded Source

Built Distribution

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

mailflat-0.7.0-py3-none-any.whl (24.5 kB view details)

Uploaded Python 3

File details

Details for the file mailflat-0.7.0.tar.gz.

File metadata

  • Download URL: mailflat-0.7.0.tar.gz
  • Upload date:
  • Size: 21.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for mailflat-0.7.0.tar.gz
Algorithm Hash digest
SHA256 2e7a237ce1ff40579dbf1d60354864261b8c199eee77a82ca307142b89ccf386
MD5 26ec8bd28b91621a0ab421352094781f
BLAKE2b-256 c233b2306e4c29242b6afa6c6bff54a9a73b96b936d1a380b7cce49bcb1be4f4

See more details on using hashes here.

File details

Details for the file mailflat-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: mailflat-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 24.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for mailflat-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8009ca5468cb3537517bb1daab8d2d9f11baf96e3898190ff0d6f6efb247808c
MD5 9f9a8c3343e17b11d48352bb8b0e2d47
BLAKE2b-256 eb8699bcf23f01aeb3e4768185254b4aacd03707eb9cff643da2f157545e2453

See more details on using hashes here.

Release history Release notifications | RSS feed

0.11.0

2 files

0.10.2

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

This release

0.7.0 This release

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.3

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

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