Python

The mcpmailer package: install, send, run the inbox loop, and wait for a reply.

The Python client is written against the standard library alone, so an agent runtime does not inherit a dependency tree just to send an email. It needs Python 3.9 or newer. The optional vault extra pulls in cryptography for AES-GCM.

Shell
pip install mcpmailer          # the client, standard library only
pip install 'mcpmailer[vault]' # adds AES-GCM for reading vault secrets

The client

Methods are synchronous and return plain dicts, which is what an LLM tool layer usually wants to hand back anyway.

Python
from mcpmailer import Mcpmailer

mm = Mcpmailer()  # reads MCPMAILER_API_KEY

staging = Mcpmailer(api_key="mmk_live_...", base_url="https://staging.mcpmailer.com")

Sending

Attachments are (filename, bytes) tuples, or dicts if you want to set the content type yourself.

Python
result = mm.send(
    to=["jane@acme.com"],
    subject="Your quote",
    body="Attached, as promised.",
    attachments=[("quote.pdf", open("quote.pdf", "rb").read())],
    track_opens=True,
)

if result["status"] == "rejected":
    print(result["reason"])  # daily_send_quota_exhausted, recipient_suppressed, ...

Reading and answering

Python
for message in mm.list_messages(unread_only=True):
    who = mm.lookup_contact(message["from"])

    mm.reply_all(message["id"], "Thanks, looking into it now.")

    if who:
        mm.remember_about_contact(who[0]["id"], "Asked about SSO pricing", message["id"])
    mm.archive(message["id"])

Also on the client: get_message, get_thread, search, get_attachment, mark_unread, forward, list_notes and create_note, list_contacts and create_contact, import_vcards and export_vcards, import_contacts_csv and the contact list calls, list_mailboxes and create_mailbox, list_domains, list_webhooks, create_webhook and update_webhook, list_secrets, get_secret, get_totp_code.

Waiting for a reply

wait_for_reply polls the thread until something inbound lands or the deadline passes, and returns None if nobody wrote back. Give it a generous timeout and treat None as a nudge to try later, not as a failure.

Python
import time

sent = mm.send(to=["jane@acme.com"], subject="Quick check", body="Does Thursday work?")
thread_id = mm.get_message(sent["messageId"])["thread_id"]

reply = mm.wait_for_reply(thread_id, timeout_seconds=900, poll_seconds=15)

if reply is None:
    print("no answer yet")  # nudge later, do not block the agent forever

Errors

RateLimited carries retry_after in seconds and subclasses McpmailerError, so one except clause catches both if you do not care about the difference.

Python
from mcpmailer import McpmailerError, RateLimited

try:
    mm.send(to=["jane@acme.com"], subject="Hi", body="Hello")
except RateLimited as error:
    time.sleep(error.retry_after)
except McpmailerError as error:
    print(error.status, error.code, error.hint)