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.
pip install mcpmailer # the client, standard library only
pip install 'mcpmailer[vault]' # adds AES-GCM for reading vault secretsThe client
Methods are synchronous and return plain dicts, which is what an LLM tool layer usually wants to hand back anyway.
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.
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
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.
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 foreverErrors
RateLimited carries retry_after in seconds and subclasses McpmailerError, so one except clause catches both if you do not care about the difference.
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)