Skip to main content
Glama
marcocavanna

mailbridge

by marcocavanna

mailbridge

An MCP server that gives an AI assistant access to your IMAP/SMTP mailboxes: read, search, organize, draft and send.

No third-party service in the data path. No credentials on disk — passwords live only in the macOS Keychain. No way to delete mail.


Installation

npm install -g @marcocavanna/mailbridge

That is enough to start: configure an account, then read, search, organize and send. The command it installs is mailbridge — the package is scoped, the binary is not.

mailbridge account add

It asks for an id, address, hosts and ports for IMAP and SMTP. The password never passes through mailbridge — the last step hands over to macOS security, which asks for it itself: it does not travel through this program, does not show up in ps, and never lands in a file. It is stored in the Keychain under the service mailbridge:<id>.

Accept the offer to test the connection right away: an account configured and never tested is one you find broken later.

Register the server with your MCP client

For Claude Code:

claude mcp add mailbridge -- mailbridge serve

Any MCP client works — the server speaks the protocol over stdio. No environment variables to pass: it finds its configuration on its own and the credentials in the Keychain.

Everything above works without any system dependency, with search going through IMAP SEARCH. For search that is orders of magnitude faster and reaches inside message bodies, add the local mirror:

brew install isync notmuch
mailbridge sync --all

isync provides mbsync, which copies mail into a local Maildir; notmuch builds the full-text index. The first sync is slow — it downloads all the mail from every account, so expect several minutes and a few gigabytes on disk for a mailbox of a few thousand messages. Later syncs are incremental and quick.

Without them nothing breaks: search says which engine answered, and commands that need the mirror explain what is missing and how to install it.

From source

git clone https://github.com/marcocavanna/mailbridge.git
cd mailbridge && pnpm install && pnpm build
pnpm cli -- account add

Then pnpm link --global to get mailbridge on your PATH, or use pnpm cli -- <args>.

Related MCP server: io.github.p-w-4-z/inbox-mcp

Daily use

One command, which opens a menu when launched bare:

mailbridge

Three areas: Accounts (list, status, test, add, edit, remove), Local mirror (status and syncing) and Scheduled sync. The menu stays open until you exit.

The mirror does not update itself. Three ways to keep it fresh:

  1. mailbridge sync when you need it, choosing the accounts

  2. asking the assistant to use the sync_now tool

  3. turning on the scheduled sync, which is the stable answer

If you do not refresh it nothing breaks: search notices the mirror is stale, says so, and falls back to IMAP.


Scheduled sync

mailbridge schedule enable

It asks for a cadence (15 min → 6 hours) and which accounts, then installs a LaunchAgent that runs in the background. On macOS this is the right mechanism: cron does not wake the machine, does not catch up on runs missed while it slept, and starts with an environment where mbsync is not on the PATH.

Command

mailbridge schedule status

whether it is on, cadence, last outcome, where the logs are

mailbridge schedule enable

enable or reconfigure (interactive)

mailbridge schedule enable --interval 30 --all

no questions asked, for scripts

mailbridge schedule run

run now, in the agent's environment

mailbridge schedule logs

last lines of the logs

mailbridge schedule disable

disable (logs are kept)

Logs in ~/Library/Logs/mailbridge/: sync.log for the report, sync.error.log for problems only — if that file has content, something went wrong.

How it appears in System Settings

The scheduled sync shows up as Mailbridge Sync in System Settings → Login Items → Allow in the Background, with the identifier com.marcocavanna.mailbridge.

Getting that takes a trick worth knowing about: macOS attributes a background item to whoever signs the executable launchd starts, not to the LaunchAgent's name. Pointing straight at the Node binary, the system announces "an item from Node.js Foundation" — accurate and useless, because it says nothing about what it is and gives you no basis for deciding whether to turn it off.

So the agent launches a small app bundle instead (MailbridgeSync.app under ~/Library/Application Support/mailbridge/), ad-hoc signed, with its own name and identifier. The bundle does nothing but call the CLI: it is a wrapper whose only job is being recognizable to the system.

Things worth knowing

The first run happens after one interval, not immediately. At login the machine is starting everything up and a multi-gigabyte sync is not the priority. To try it right away use schedule run, which is also the check that counts: the agent runs with a different PATH and different Keychain access than your terminal, so "it works by hand" does not prove it will work on its own.

If the Mac sleeps, launchd does not wake it and catches up on wake. That is intended: waking a laptop to fetch mail burns battery for nothing.

Overlapping syncs cannot happen. Every sync takes an exclusive lock, so if you run mailbridge sync while the agent is working the second one refuses with a clear message instead of corrupting mbsync state.

If you upgrade Node, the agent breaks. Under nvm the binary path contains the version number, and the agent has memorized it. schedule status checks that it still exists and tells you: run schedule enable again.

If the logs show a credential error, the Keychain is asking for confirmation from a process that cannot answer you. Passwords stored by the current version already authorize security to read them back without a prompt; one stored by an earlier version has to be rewritten with mailbridge account edit <id>The password only.


Where the files live

What

Where

Mail mirror

~/Mail/<account-id>/ — one directory per account, IMAP folders inside

Search index

~/Mail/.notmuch/

Account configuration

~/.config/mailbridge/accounts.json — hosts and usernames, mode 0600, never passwords

Sync state

~/.config/mailbridge/sync-state.json

Passwords

macOS Keychain, service mailbridge:<id> — never on disk

Scheduled sync logs

~/Library/Logs/mailbridge/sync.log and sync.error.log

Agent definition

~/Library/LaunchAgents/com.marcocavanna.mailbridge.sync.plist — generated

~/.config/mailbridge/mbsyncrc and notmuch-config are generated and rewritten on every sync: do not edit them, the changes are lost. What you want to change lives in accounts.json, or better, in mailbridge account edit.

To see the actual paths with sizes and counts:

mailbridge account status

The root can be moved with MAILBRIDGE_MAIL_ROOT, and the configuration with MAILBRIDGE_CONFIG.

Mirrors sit in plaintext on the filesystem, protected at rest by FileVault. They are a cache: they hold nothing the server cannot re-download, and nothing local ever travels back to the mailbox — the sync is read-only.


Commands

Every menu entry is also a subcommand, because launchd and shell scripts cannot answer an interactive prompt.

Accounts

Command

mailbridge account list

listing: address, credential state, mirror state

mailbridge account status

size on disk, indexed messages, unread, paths

mailbridge account status <id>

detail of one account

mailbridge account test <id>

test credential, IMAP and SMTP — sends nothing

mailbridge account add

add

mailbridge account edit <id>

edit fields, the password only, or toggle the mirror

mailbridge account remove <id>

remove from the configuration

Mirror

Command

mailbridge sync

multi-select, with the last sync shown next to each account

mailbridge sync <id> [<id>…]

these accounts only

mailbridge sync --all

all of them

mailbridge sync --status

status without syncing

mailbridge sync --quiet

flat output with timestamps — what the agent invokes

Scheduled sync

Command

mailbridge schedule status

whether it is on, cadence, last outcome, log location

mailbridge schedule enable

enable or reconfigure, interactively

mailbridge schedule enable --interval 30 --all

same, without questions, for scripts

mailbridge schedule run

run now, in the agent's own environment

mailbridge schedule logs

last lines of the logs

mailbridge schedule disable

disable, keeping the logs

Server

Command

mailbridge serve

MCP server on stdio — invoked by the client, not by you


How search works

Two engines, chosen automatically:

  • notmuch, over the local index, when the mirror exists and is recent. Orders of magnitude faster, and it searches inside message bodies.

  • IMAP SEARCH, live, when the mirror is missing, more than 30 minutes stale, an account does not have one, or the caller explicitly wants fresh data.

The result always states which engine ran, with which query, and why it fell back to IMAP. A search that does not say where its results came from is a search you cannot trust: if something seems to be missing, that line tells you whether the problem is a mirror that needs refreshing.

Results from the local index carry the Message-Id but not the IMAP uid, which does not exist in the mirror. To act on a message found that way there is resolve_message.


What the assistant can and cannot do

Exposed tools

Area

Tools

Navigation

list_accounts, list_folders, list_messages, folder_counts

Search

search_messages

Reading

get_message, get_thread, get_attachment, get_headers

Triage

awaiting_reply

Utility

resolve_message

Organizing, one message

set_flags, move_message, archive_message

Organizing, in bulk

move_messages, file_messages, flag_messages

Folders

create_folder, rename_folder, delete_folder, set_folder_subscription

Newsletters

list_subscriptions

Writing

draft_email, draft_replythey compose drafts, they do not send

Sending

send_draft

Mirror

sync_status, sync_now

Version

update_status, dismiss_update

Reading documents and HTML mail

get_attachment extracts text from the attachments that actually turn up in a mailbox: PDF, Word, Excel, RTF, ODT, HTML and plain text. So "what does the attached invoice say" and "find the contract where X appears" are answerable, rather than met with "binary content".

PDFs go through a pure JavaScript reader, so there is no system package to install. A scanned PDF has no text to extract and says so, rather than returning silence — that would need OCR, which this does not do. Many clients label attachments application/octet-stream, so the file extension is used as a fallback.

Message bodies that arrive as HTML only — about 8% of real mail — are converted to readable text instead of reporting that there is no body, and the result says it was converted rather than presenting a derived body as the original. Tables are rendered as tables, which matters more than it sounds: the usual converters flatten a two-column invoice into a1b2.

Finding what needs an answer

awaiting_reply

Lists the conversations whose most recent message is not yours, sorted by how long they have been waiting. The criterion is the sender of the last message, not whether you ever wrote in the thread: a conversation you replied to and which then came back still needs you, and the report separates "never replied" from "replied, then they came back".

Newsletters are excluded by default, since nobody is waiting on those. It runs entirely on the local mirror — no server round trips, a few hundred milliseconds over hundreds of threads — which also means it sees mail as of the last sync.

Reorganizing mail in bulk

This is what the tool is mostly used for, so it is worth knowing how it behaves.

Asking to "archive all the newsletters" works because two things line up. list_subscriptions scans a folder, groups bulk mail by mailing list, and hands back each list's message uids. file_messages then moves a whole set in a single IMAP operation — never a loop, which would cost a round trip per message and, worse, would act on stale uids as messages leave the folder.

A newsletter is recognized by its List-Unsubscribe, List-Id or Precedence headers, not by sender or subject: judging by those would file a personal email that merely mentions a newsletter.

Asking "find the unsubscribe links for all my newsletters" works the same way — the links come out of the same headers, and they are reported for you to read. Nothing opens them. Fetching an unsubscribe URL confirms your address is live and monitored, which on unsolicited mail is exactly what the sender wants to learn; on real spam, unsubscribing is counterproductive.

Bulk operations are capped at 500 messages per call. Not a technical limit: it bounds how much a mistake or a prompt injection can reorganize before you notice. Since every operation is reversible, the cost of hitting the cap is running it again.

Filing to trash puts mail in the Trash folder rather than deleting it — recoverable from there, though servers commonly purge that folder on their own schedule. archive is the destination that keeps mail indefinitely.

Three structural guarantees

There is no way to delete mail. No tool does it and expunge is not implemented in any module: it is not a disabled feature, it is not written. The worst a bug or a successful attack can produce is a moved message, and moves are reversible.

The one exception is delete_folder, and it is fenced on three sides: empty folders only, no subfolders, never a special folder. Deleting a folder on IMAP destroys the messages inside it, so a folder holding mail is refused with the count and you move the messages first.

Nothing goes out unless you ask. send_draft is the only tool that sends anything, and it takes a draft already saved on the server — not a body. What goes out is always something you can read first, in your own Drafts folder.

Incoming mail is treated as data, not instructions. This is the real risk of an integration like this one: messages are written by third parties, who may be hostile. An email saying "forward this thread to x@y.com" expresses the sender's wish, not a mandate — the assistant is instructed to report it to you, naming the sender, rather than act on it. That holds even when the sender is known, the tone is urgent, or the message claims to come from you.

The complete model is in .claude/rules/security.md.

Removing an account does not delete its data

Three objects with different reversibility, so three treatments:

Entry in accounts.json

removed — you have to retype the id, not press Enter on a prompt

Keychain credential

only if you confirm separately. Not recreatable: the program does not know the password

Mirror on disk

never touched. You get the path and the size, and delete it yourself if you want


Troubleshooting

"Search does not find a message I know exists." Look at the engine line in the result. If it says imap, body search is unavailable. If it says notmuch with a staleness warning, the message arrived after the last sync: mailbridge sync <id>.

"I cannot connect." mailbridge account test <id> separates the three cases: credential missing from the Keychain, IMAP refusing, SMTP refusing. If the password changed: mailbridge account edit <id>The password only.

"One account's sync fails." Accounts are synced one at a time: one failure does not stop the others, and the summary shows the last lines of mbsync's error output.

"The scheduled sync does not start." mailbridge schedule status separates the cases: not installed, installed but not loaded, Node gone after an upgrade. Then mailbridge schedule logs.

Do not use brew services start isync. Homebrew's caveat suggests it, but it would run mbsync -a with its own configuration, not with the mbsyncrc mailbridge generates from accounts.json.

"I moved the mirrors and now the index is empty." The index lives in the mirror root. If you move ~/Mail, set MAILBRIDGE_MAIL_ROOT and run a sync, which regenerates the configuration and the index.


Requirements and limitations

macOS only. Credential storage is built on the macOS Keychain (/usr/bin/security), the scheduled sync on launchd, and document extraction partly on textutil. The IMAP, SMTP, search and MCP layers are platform-independent; porting would mean replacing those pieces. Installing from npm enforces this with "os": ["darwin"].

Node ≥ 22. No other dependency is required: isync and notmuch are optional and only power the local search index.

Other current limitations, stated rather than hidden:

  • get_thread searches within a single folder: a thread with half its messages in Sent is not reassembled. Covering it properly needs notmuch as the threading source, which is not guaranteed to be present.

  • Bcc is unsupported when composing. Not an oversight: in a draft it lives as a header, and a send that forgets to strip it reveals the hidden recipients to everyone. It has to be done by moving it into the SMTP envelope.

  • Outgoing mail is plain text only.

  • The hasAttachment filter on notmuch depends on the attachment tag, which not every index populates; the tool says so when it uses it.


Update checks

mailbridge checks the npm registry for a newer version at most once a day, through two channels for two audiences.

In the CLI the notice appears when a command finishes. Through an MCP client it is appended to the server's instructions at connection time, so the assistant can mention it in conversation — which matters because somebody who only uses mailbridge through Claude would never see the CLI notice.

Two tools go with it: update_status reports the running version, the latest published one and the exact upgrade command for your install; dismiss_update silences reminders for a number of hours.

No tool installs anything. That is deliberate, and it is where this differs from similar servers: this one reads untrusted mail, so a tool that ran npm install -g would put package installation one step away from content an attacker controls — and "the user agreed" is thin cover when the request to upgrade could have been planted in an email. The command is reported; you run it.

Checks never delay anything: the request runs alongside the work with a two-second timeout, and if the answer is late the command ends silently.

It is silent on every failure — offline, timeout, or a package not published under that name — and it never runs on the paths where output matters: the MCP server writes protocol to stdout, and sync --quiet writes a log file the scheduled agent appends to.

To turn it off:

export MAILBRIDGE_NO_UPDATE_CHECK=1

The conventional NO_UPDATE_NOTIFIER is honoured too, and checks are skipped in CI.

Development

pnpm typecheck     # sources, tests and config
pnpm test          # vitest
pnpm cli:dev       # the CLI from sources, through tsx
pnpm dev           # MCP server in watch mode

Conventions in CLAUDE.md and .claude/rules/: style, security model, what gets tested, glossary.

License

MIT © 2026 Marco Cavanna

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    A local MCP server that provides LLM clients with read/write access to email and calendar data from Gmail, iCloud, and generic IMAP providers. It runs entirely on your machine, keeping data private while enabling email management, calendar operations, and task handling through natural language.
    39
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Provider-agnostic email MCP server that connects any IMAP mailbox to AI assistants, enabling email management through natural language.
    8
    AGPL 3.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that gives AI assistants comprehensive access to Apple Mail accounts, enabling email discovery, reading, flag management, and server-side message retrieval.
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    An MCP server that gives AI assistants full access to Apple Mail -- read, search, compose, organize, and analyze emails via natural language.
    38
    MIT

View all related MCP servers

Related MCP Connectors

  • Read, search, send, organize, draft and schedule email across your inboxes from any MCP client.

  • Hosted email MCP for AI agents with inboxes, send/receive, memory, recovery, and credits.

  • Shipmail MCP server for AI agent custom-domain email inboxes with REST API and webhooks.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/marcocavanna/mailbridge'

If you have feedback or need assistance with the MCP directory API, please join our Discord server