Skip to main content
Glama
bdi-admin

mcp-mail-bridge

by bdi-admin

mcp-mail-bridge

An MCP server that bridges an LLM tool client to a mailbox over IMAP and SMTP. It exposes a small, deliberately read-mostly set of tools: health checks, mailbox listing, message listing/fetching, and idempotent send/reply. There is no message deletion, flag mutation, or archive manipulation of any kind.

Protocol requirements. The implementation targets:

  • IMAP4 (imaplib.IMAP4_SSL) for read operations, SMTP (smtplib.SMTP_SSL) for send/reply — both over implicit TLS, not STARTTLS.

  • Username/password authentication via the standard IMAP LOGIN command and SMTP AUTH (through login()); no OAuth flow is implemented.

  • Configurable IMAP and SMTP host/port, defaulting to the implicit-TLS convention (993 / 465).

It has been tested end-to-end against one real-world IMAP/SMTP mailbox meeting those requirements. Any other provider that offers implicit-TLS IMAP4 and SMTP with username/password login should work by protocol conformance, but that broader interoperability is a design expectation, not something independently verified here — a provider that only offers STARTTLS on port 587, with no implicit-TLS option, will not work without modifying _smtp()/_imap() in core/mail_core.py.

Structure

mcp_mail_bridge/
  core/
    types.py          frozen contract dataclasses and enums
    errors.py         typed, sanitized operation failures
    credentials.py    call-time protected-file credential provider
    idempotency.py    SQLite key -> SendResult bookkeeping only
    drafts.py         SQLite-backed persistent reply drafts
    mail_core.py      per-operation IMAP/SMTP, MIME, replies and send outcomes
  adapters/mcp/
    schema.py         MCP input models and JSON conversion
    server.py         six delegating tools and stdio entry point
tests/
  test_contract.py    fake IMAP / real smtplib with in-memory socket
  test_adapter.py     schema, conversion, errors and real stdio discovery

Core does not import MCP, HTTP or CLI frameworks. Only mail_core.py knows imaplib/smtplib. The adapter never obtains a password. Connections use verified TLS and close after each operation. Inbound selection is read-only and every fetch uses BODY.PEEK; replies fetch only the threading/address header fields. No STORE, APPEND, EXPUNGE, MOVE or mark-seen API exists.

Related MCP server: imap-mcp

MCP capabilities

The V0 tools are mail_health, mail_list_mailboxes, mail_list_messages, mail_fetch_message, mail_send, and mail_reply. V1 additionally provides mail_prepare_reply, mail_update_draft, mail_get_draft, mail_send_draft, and mail_discard_draft. Send and reply require an idempotency key. UIDVALIDITY is checked before fetching a MessageRef, so a stale reference (for example, after a mailbox re-sync) fails closed instead of silently fetching the wrong message.

mail_prepare_reply resolves the source message's recipient and threading headers once using a read-only BODY.PEEK fetch, then stores an opaque draft ID. A draft's source reference, recipients, subject, Message-ID, In-Reply-To, and References are immutable. Only its text and HTML body can be updated. mail_send_draft constructs the outgoing message entirely from that persisted context and body; it does not re-fetch IMAP or reconstruct the reply target. Draft bodies persist in the same local SQLite state file as the idempotency results, so that file must be treated as confidential application data. Sent and ambiguous drafts are retained and immutable; a definitively failed draft remains pending for editing or a new-key retry. There are no standalone persistent outbound drafts in V1: mail_send remains the direct path for new mail.

Credential handling model

Credentials are never held by the MCP adapter and never embedded in the package, database, or logs. A CredentialProvider reads a password fresh from a protected local file only at the moment a core operation needs it.

To be precise about what that guarantees: the code sets its local variable holding the password to None immediately after use (see the password = None sites in core/mail_core.py). That releases the application's own reference so the value isn't held in scope longer than necessary and isn't copied into the database or logs — it is not a claim that CPython overwrites or wipes the underlying bytes in process memory, that garbage collection is immediate, or that the string can't still exist in memory (e.g. via string interning, swap, or a core dump) after the reference is dropped. If you need actual memory-scrubbing guarantees, this package does not provide them.

The default entry point (mcp_mail_bridge/adapters/mcp/server.py) is configured entirely through environment variables, with no live values baked in:

  • MAILBRIDGE_USERNAME — mailbox login (default user@example.com)

  • MAILBRIDGE_CREDENTIAL_PATH — path to a file containing only the password (default /etc/mcp-mail-bridge/credential, e.g. /path/to/credential)

  • MAILBRIDGE_IMAP_HOST / MAILBRIDGE_SMTP_HOST — your provider's IMAP and SMTP hostnames (defaults imap.example.com / smtp.example.com, which do not resolve to a real server — you must set these)

  • MAILBRIDGE_IMAP_PORT / MAILBRIDGE_SMTP_PORT — implicit-TLS ports (default 993 / 465)

  • MAILBRIDGE_IDEMPOTENCY_PATH — SQLite database path (default /var/lib/mcp-mail-bridge/idempotency.sqlite)

  • MAILBRIDGE_OPERATOR_NAME — the party named in the send/reply tool descriptions and in the AMBIGUOUS outcome detail as the one who must externally verify an uncertain send before any retry (default user; set it to a specific name or role if your deployment wants the LLM client to address someone in particular)

The credential file's permissions and ownership are the deploying environment's responsibility; this package does not create, chmod, or chown it. The parent directory of the idempotency database must exist and be writable by the execution identity.

MailCore's host/port/operator values are ordinary constructor parameters, not hardcoded anywhere in core/; server.py's main() is simply one choice of how to source them (environment variables).

Read-only behavior

Listing and fetching never set the Seen flag or otherwise mutate mailbox state. Listing returns newest UIDs first, up to the query limit, and peek-fetches those messages transiently only to obtain accurate MIME attachment metadata — no attachment bytes are persisted or exposed as downloadable content. IMAP SINCE filtering is day-granular, matching the IMAP protocol's own date resolution.

Send/reply safety and idempotency

  • SENT: the final DATA response confirmed SMTP acceptance (250), not recipient inbox delivery.

  • FAILED: a typed AUTH, NETWORK_PRE_SUBMISSION, or SMTP_REJECTED result.

  • AMBIGUOUS: acceptance may have occurred but definitive confirmation was lost. Callers must stop and verify the original attempt out of band (for example, by checking the Sent folder or the recipient) before any further send of that logical message. A new idempotency key is never authorization to retry an AMBIGUOUS send. This safeguard is not configurable away — only the wording of who to escalate to (MAILBRIDGE_OPERATOR_NAME) is.

  • Every recorded result, including FAILED and AMBIGUOUS, is returned for a repeated key without opening a second SMTP connection. There are no automatic retries.

  • Database records contain only SendResult fields — no mail content or credentials are ever written to the idempotency store.

  • The idempotency store's get/put contract protects recorded results. It is not an exactly-once guarantee across process crashes, failed database writes, or simultaneous independent processes. Serialize callers for correct behavior; if a process or adapter fails around a send, stop and inspect before sending again.

  • The stdio adapter processes synchronous core operations serially in its event loop. It creates no background mailbox worker or connection pool.

  • On any recipient rejection, the send aborts before DATA, so a single SendResult never conceals a partial-recipient submission. Bcc recipients appear only in the SMTP envelope, never in message headers.

  • Public mailbox names are Unicode, translated to/from IMAP modified UTF-7. MessageContent header keys are lowercase. SMTP envelope addresses are ASCII; Unicode subjects and MIME bodies are supported. SMTPUTF8 negotiation is not implemented.

Installation / setup

Python 3.11 or later:

python -m venv .venv
.venv/bin/python -m pip install .

On Windows use .venv\Scripts\python.exe. Set the environment variables above (or edit main() in server.py for your own wiring) and run the mcp-mail-bridge console script, or python -m mcp_mail_bridge.adapters.mcp.server, to serve MCP over stdio.

Testing

.venv/bin/python -m unittest discover -s tests -v

Tests use synthetic data, temporary SQLite databases, and fake protocol connections — no real IMAP/SMTP connection or email submission is made. SMTP boundary tests exercise the standard library's DATA normalization and dot-stuffing helpers plus its response parser against an in-memory socket. DATA content and terminator are separate writes, so a body-write failure is FAILED while a terminator-write failure is AMBIGUOUS. The stdio test initializes/discovers tools and submits only an invalid draft that must fail before any credential access.

Limitations

  • No message-list pagination: mail_list_messages returns up to limit newest messages per call with no cursor for paging further back.

  • No SMTPUTF8 negotiation for non-ASCII envelope addresses.

  • No exactly-once delivery guarantee across process crashes or concurrent callers (see idempotency notes above); a single serialized caller is required for the stated guarantees to hold.

  • IMAP SINCE filtering is day-granular, not timestamp-granular.

  • No STARTTLS support — only implicit TLS on the configured ports.

  • No OAuth; only username/password login.

Implementation references: Python's smtplib documentation and the official MCP Python SDK.

License

Apache License 2.0.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables LLMs to read, search, and manage emails via IMAP with secure, read-only access to email accounts.
    6
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables email management via IMAP and SMTP with multi-account support, safe sending with confirmation, and read-only modes.
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Read-only MCP server for IMAP email access, enabling AI agents to read, search, and monitor email without sending or deleting messages.
    27 npm
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Enables read-only, provider-agnostic email access over IMAP, allowing users to list folders, search and read messages, and download attachments without ever marking messages as read.
    6
    MIT