postbus-mcp
Allows linking Gmail mailboxes via IMAP/SMTP or the Gmail API and searching, reading conversation threads, and sending mail.
Allows linking iCloud Mail accounts via IMAP/SMTP and searching, reading threads, and sending mail.
Allows linking Proton Mail accounts through Proton Bridge and searching, reading threads, and sending mail.
Allows linking Zoho Mail accounts via IMAP/SMTP and searching, reading threads, and sending mail.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@postbus-mcpsearch my inbox for unread emails from Alice and summarize them"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
postbus-mcp
A self-hosted MCP server that lets you and a handful of people around you work with your mailboxes from Claude or any other MCP client: search, read whole conversations, send, reply, forward, and keep the whole thing tidy with folders and labels.
Works with any IMAP/SMTP provider — Gmail, Outlook, Fastmail, your own mail server — using a plain app password. No Google Cloud project, no OAuth verification, no test-user limit.
One instance serves multiple users. Everyone gets their own API token and sees only their own mailboxes. You host it, you hand out the tokens; there is no open sign-up.
Claude / MCP client
│ Authorization: Bearer <token>
▼
POST /mcp ──► postbus-mcp ──► SQLite (users + encrypted app passwords)
│
├──► IMAP (imapflow) search, read, threads, folders
└──► SMTP (nodemailer) sending, replies, forwardsContents
Related MCP server: simple-email-mcp
How it works
Multi-tenant, but small. One SQLite file with two tables: users (id plus a
hash of the API token) and mail_accounts (each user's mailboxes, with the app
password encrypted). No separate database service to run.
Isolation lives in the query, not in a check afterwards. Every MCP session
belongs to exactly one user, decided by the bearer token. The MCP server is
built per request around that user, and every database query carries the
user_id in its WHERE. Someone else's alias simply does not exist in your
session.
Provider interface. The tool layer talks to a generic MailProvider and
knows nothing about IMAP or Gmail. ImapSmtpProvider is the main
implementation, with an optional GmailApiProvider alongside it. Adding a third
takes no changes to the tools — see Adding a provider.
Quick start
With Docker (recommended)
git clone https://github.com/HalloSouf/postbus-mcp.git
cd postbus-mcp
cp .env.example .env
openssl rand -hex 32 # put the result in .env as MASTER_KEY
docker compose up -d --build
docker compose exec postbus node dist/cli/add-user.js "Soufiane"That last command prints an API token exactly once. Save it right away.
Locally with Node (24 or newer)
npm install
cp .env.example .env
openssl rand -hex 32 # put the result in .env as MASTER_KEY
npm run build
npm run add-user -- "Soufiane"
npm startThe server listens on http://localhost:3000/mcp. A GET /health returns
{"status":"ok"}, which is handy for an uptime check.
Users and tokens
You hand out tokens yourself; there is no self-service registration.
Command | What it does |
| Creates a user and prints the token (once) |
| Shows users, mailbox counts and status |
| New token; the old one stops working immediately |
| Deletes the user and all their mailboxes |
In Docker, run the same scripts as node dist/cli/<script>.js:
docker compose exec postbus node dist/cli/list-users.js
docker compose exec postbus node dist/cli/rotate-token.js WvDnhafdM5yQOnly a SHA-256 hash of each token is stored, so a lost token cannot be looked up — rotate it instead.
Connecting your client
Claude Desktop
Claude Desktop speaks stdio, so put
mcp-remote in between. In
claude_desktop_config.json:
{
"mcpServers": {
"postbus": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://mcp.example.com/mcp",
"--header",
"Authorization: Bearer pb_YOUR_TOKEN_HERE"
]
}
}
}That file lives at
~/Library/Application Support/Claude/claude_desktop_config.json on macOS and
%APPDATA%\Claude\claude_desktop_config.json on Windows. Restart Claude Desktop
after editing it.
Claude Code
claude mcp add --transport http postbus https://mcp.example.com/mcp \
--header "Authorization: Bearer pb_YOUR_TOKEN_HERE"Other clients
Anything that speaks Streamable HTTP works: endpoint POST /mcp, token as
Authorization: Bearer <token>. The server runs stateless — no session ids, no
server-side stream — so a GET /mcp deliberately returns 405.
Linking a mailbox
You do this in the conversation, with your own token. No terminal needed:
Link my Gmail as "personal", address souf@gmail.com, app password abcd efgh ijkl mnop
Claude then calls add_mail_account. The connection is tested first (IMAP and
SMTP both); nothing is stored until both work.
Creating an app password
Provider | Where | Note |
Gmail / Workspace | Requires 2FA on the account | |
Outlook / Microsoft 365 | Requires 2FA; an admin can block IMAP | |
Fastmail | Settings → Privacy & Security → App passwords | Pick "Mail (IMAP/SMTP)" |
iCloud | https://account.apple.com → App-specific passwords | Requires 2FA |
Own server | n/a | Your mail password, or a dedicated account |
Never use your normal password when the provider offers app passwords.
Host and port
For known providers postbus-mcp fills these in — you only supply alias, email and app password:
Gmail, Google Workspace, Outlook, Hotmail, Microsoft 365, Fastmail, iCloud, Yahoo, Zoho, Proton (via Bridge).
For anything else, pass them yourself:
imap_host: imap.yourdomain.com imap_port: 993 (TLS)
smtp_host: smtp.yourdomain.com smtp_port: 465 (TLS) or 587 (STARTTLS)Ports 993 and 465 use TLS from the first byte; on other ports STARTTLS is used
when the server offers it. If that assumption is wrong for your server, pass
imap_secure or smtp_secure explicitly.
Available tools
Mailboxes
Tool | What it does |
| Lists your mailboxes with alias and email address |
| Links an IMAP/SMTP mailbox with an app password (tests the connection first) |
| Unlinks a mailbox and wipes the stored app password |
Reading
Tool | What it does |
| Searches with Gmail-style syntax; returns an |
| Full content of one message: headers, body, attachment metadata |
| Every message in a conversation, oldest first |
Writing
Tool | What it does |
| Sends a new message straight away (cc, bcc, reply-to, html) |
| Replies in the same conversation; recipients, subject and quoting are filled in |
| Forwards a message, with the original attached as |
Organising — these all take a list of ids, so one call can handle twenty messages
Tool | What it does |
| Out of the inbox, nothing deleted |
| To the trash, still recoverable |
| Into any folder |
| Read, unread, starred, unstarred |
| Adds or removes Gmail labels |
| Gives a Gmail label a colour |
| Every folder, with the special ones marked |
| New folder or Gmail label, nesting included |
| Renames one, keeping its messages |
| Removes one; refuses Sent, Trash and the like |
Every tool only ever touches mailboxes belonging to the user behind the token.
There is deliberately no permanent delete. trash_messages moves mail to the
trash and nothing empties it, so anything Claude removes can be recovered from
your own mail client.
Folders or labels
Gmail has labels, everyone else has folders, and the two behave differently: a message carries several labels at once but sits in exactly one folder. The tools follow whichever the server actually has.
move_messages,list_foldersandcreate_folderwork everywhere. On Gmail a folder is a label, and moving means swapping the inbox label for another one.label_messagesandset_label_colorare Gmail only, andset_label_coloradditionally needs the mailbox linked through the Gmail API rather than IMAP: no IMAP extension carries a label colour. Over IMAP a label is just a name.archive_messagesmeans "out of the inbox" on both: Gmail drops the inbox label, other servers move the message to Archive.
Search syntax
search_emails uses Gmail-style syntax. For Gmail mailboxes your query goes
to Gmail unchanged (via X-GM-RAW), so anything that works in the Gmail search
bar works here. For other IMAP servers it gets translated:
Term | Gmail | Other IMAP |
| ✅ | ✅ |
| ✅ | ✅ |
| ✅ | ✅ |
| ✅ | ✅ |
| ✅ | ✅ |
| ✅ | ✅ (filtered afterwards) |
| ✅ | ✅ (via SPECIAL-USE) |
| ✅ | ✅ |
| ✅ | ⚠️ one combined text term |
| ✅ | ❌ ignored |
Examples:
from:boss@company.com is:unread newer_than:7d
subject:"march invoice" has:attachment
in:sent to:client@example.com older_than:1mAn empty query returns the newest messages in the inbox.
Threading
Every search_emails result carries a threadId, and get_thread uses it to
pull the whole conversation — chronological, with sender, subject, date and body
per message.
There are two ways that happens, depending on what the server can do:
Gmail (
X-GM-THRID) and RFC 8474 servers (OBJECTID) hand out a stable thread id themselves. We use it directly, and thethreadIdlooks likesrv:1829384756.Every other IMAP server has no concept of threads. There we reconstruct the conversation from the standard
Message-ID,In-Reply-ToandReferencesheaders: the first id in that chain is the root of the thread. ThosethreadIdvalues start withref:.
When fetching, we look in the "all mail" folder if the server has one, and otherwise across Inbox, Sent and Archive — so your own replies end up in the conversation too.
Deploying behind Traefik
docker-compose.yml in this repo is a working example. The core of it:
services:
postbus:
build: .
restart: unless-stopped
environment:
MASTER_KEY: ${MASTER_KEY:?set MASTER_KEY in .env}
DATABASE_PATH: /data/postbus.db
TRUST_PROXY: "true"
volumes:
- postbus-data:/data
networks: [proxy]
labels:
traefik.enable: "true"
traefik.docker.network: proxy
traefik.http.routers.postbus.rule: Host(`${PUBLIC_HOST:-mcp.example.com}`)
traefik.http.routers.postbus.entrypoints: websecure
traefik.http.routers.postbus.tls.certresolver: letsencrypt
traefik.http.services.postbus.loadbalancer.server.port: "3000"Things to watch:
Set
PUBLIC_HOSTin.envto your own hostname; that is the only place the domain appears, so the compose file itself stays untouched.The
proxynetwork has to exist (docker network create proxy) and Traefik has to be on it.The container publishes no port of its own: only Traefik can reach it.
TRUST_PROXY=truelets Express trust theX-Forwarded-*headers. It is off by default: only turn it on when something like Traefik really is in front, because otherwise any client can claim any address.Terminate TLS at Traefik. Tokens travel as bearer credentials; without HTTPS they are in the clear.
The
postbus-datavolume holds the database with every encrypted app password. See below for how to back it up.
Backups
The database runs in WAL mode, so copying the file out of a running container can catch it mid-transaction. Ask SQLite for a consistent copy instead:
docker compose exec postbus \
node -e "require('better-sqlite3')(process.env.DATABASE_PATH).backup('/data/backup.db')"
docker compose cp postbus:/data/backup.db ./postbus-$(date +%F).dbKeep the MASTER_KEY somewhere else. The backup is useless without it, and
together they are a complete copy of everyone's mailbox credentials.
To restore, stop the container, put the file back as /data/postbus.db (delete
any -wal and -shm next to it), and start with the same MASTER_KEY.
Note that the server refuses to open a database whose schema is newer than the build — rolling the image back further than the database will fail loudly rather than corrupt anything.
Security
MASTER_KEY. App passwords and refresh tokens are stored with AES-256-GCM,
each with its own IV, and sealed against the row they belong to — a ciphertext
moved to another user's row will not decrypt. The key must be 64 hex characters
(openssl rand -hex 32); a passphrase is not accepted, because stretching one
against a salt baked into the source would be the same salt in every install.
The server refuses to start without the key. Lose it and everyone has to link
their mailboxes again, so keep it apart from the database backup.
Tokens. Only the SHA-256 hash is stored. Share them over a channel you
trust and rotate when in doubt (npm run rotate-token).
Isolation. Every query on mail_accounts filters on user_id, and the MCP
server is built per request around a single user, so there is no session store
that could mix people up.
Transport. IMAP and SMTP connections on a plain port demand STARTTLS instead of falling back to cleartext when a server does not offer it, which is what a downgrade attack arranges. Loopback is exempt, for local bridges such as Proton Mail Bridge.
Rate limiting. 60 tool calls per token per minute
(RATE_LIMIT_PER_MINUTE), and at most 20 linked mailboxes per user
(MAX_ACCOUNTS_PER_USER).
Logging. One JSON line per tool call on stdout: tool, user id, duration, outcome and error kind. Never mail content, addresses or search queries.
Untrusted content. Message bodies and snippets reach the model inside
explicit markers saying they are data. That is a mitigation, not a fix: a
message can still try to talk the model into sending mail, which is why
send_email is marked destructive so clients ask before calling it. Treat
anything a mailbox returns as attacker-controlled.
What this is not. No fine-grained permissions, and no audit trail beyond the log line above. This is built for a handful of people you know, behind TLS. Do not open it up to an unknown audience.
Adding a provider
The tool layer only ever talks to MailProvider from src/types.ts:
interface MailProvider<A extends MailAccount = MailAccount> {
readonly id: ProviderId;
verify(account: A): Promise<void>;
search(account: A, query: string, maxResults: number): Promise<MessageSummary[]>;
getMessage(account: A, messageId: string): Promise<MessageDetail>;
getThread(account: A, threadId: string): Promise<MessageDetail[]>;
send(
account: A,
to: string,
subject: string,
body: string,
options?: SendOptions,
): Promise<string>;
}A provider receives a fully resolved account, credentials decrypted. Alias lookup happens in the tool layer, so a provider cannot reach outside the session's user.
To add one:
Extend
ProviderIdand theMailAccountunion insrc/types.ts.Write
src/providers/<name>/provider.tswith a class implementing the interface.Add one line to the map in
src/providers/registry.ts.Make sure an account of that type can reach the database: a
save<Name>Account()insrc/db/accounts.ts(secrets go throughencryptSecret), plus a way to link one — an extra tool next toadd_mail_account, or a CLI script.
The existing tools (search_emails, get_message, get_thread, send_email)
need no changes. See also CONTRIBUTING.md.
Optional: Gmail through the API instead of IMAP
The repo carries a second provider that reaches Gmail through the Gmail API rather than IMAP/SMTP. You will almost never need it — IMAP with an app password does the same job with far less ceremony. It is only useful when your organisation blocks IMAP but allows the API.
Create a project at https://console.cloud.google.com.
APIs & Services → Library → search for "Gmail API" → Enable.
APIs & Services → OAuth consent screen → type External → fill in a name and support email.
Add the addresses you plan to link under Test users.
Credentials → Create credentials → OAuth client ID → type Desktop app.
Put the client id and secret in
.env:GOOGLE_CLIENT_ID=xxxxxxxxxxxx.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxx OAUTH_CALLBACK_PORT=53682Link a mailbox. This runs on the admin's machine, because Google sends the callback to
localhost:npm run list-users # look up the user id npm run link-gmail -- <user-id> work
Scopes used: gmail.readonly, gmail.send, gmail.compose, gmail.labels.
Note: while the OAuth consent screen is set to Testing, refresh tokens expire after 7 days and you have to link again. That only stops once the consent screen goes to In production, which for these scopes requires Google verification. This is exactly why IMAP with an app password is the main route.
Development
npm install
npm run dev # server with hot reload (tsx watch)
npm test # unit tests (vitest)
npm run typecheck # src + tests
npm run format # prettier across the repo
npm run build # into dist/The tests in tests/ run in half a second and touch nothing outside the
process: SQLite runs in memory and no connection leaves the machine. They cover
the logic that can go wrong quietly — search query translation, encoding message
and thread ids, parsing and composing MIME, encrypted storage, the separation
between users, the bearer middleware, the rate limiter, and the header
injection and content-fencing defences.
What they do not cover is talking to a real mail server. For that, run GreenMail locally:
docker run -d --rm --name greenmail -p 3143:3143 -p 3025:3025 \
-e GREENMAIL_OPTS='-Dgreenmail.setup.test.imap -Dgreenmail.setup.test.smtp -Dgreenmail.users=souf:secret@postbus.test -Dgreenmail.hostname=0.0.0.0' \
greenmail/standalone:2.1.0Then link a mailbox with imap_host: 127.0.0.1, imap_port: 3143,
smtp_host: 127.0.0.1, smtp_port: 3025, username: souf,
app_password: secret.
GreenMail speaks no Gmail extensions. The branch of the code that uses
X-GM-RAWandX-GM-THRIDcan only be exercised against a real Gmail mailbox.
GitHub Actions runs the same checks on every push and pull request: formatting,
types, npm audit over the production dependencies, and the tests. CI and the
container both run Node 24.
The image is not built in CI. Deployment builds from the repository, so a broken
Dockerfile surfaces there rather than here — check it locally with
docker compose up -d --build when you touch it.
Project layout
src/
├── index.ts entry point: turns a startup failure into a message
├── main.ts startup: check MASTER_KEY, open the db, listen
├── config.ts environment configuration, validated with zod
├── crypto.ts AES-256-GCM for secrets, hashing for tokens
├── net.ts which mail hosts we are willing to connect to
├── log.ts one JSON line per event, on stdout
├── types.ts MailProvider plus every shared type
├── db/ SQLite: migrations, users, mail_accounts
├── http/ Express app, bearer auth, rate limit, MCP transport
├── providers/
│ ├── registry.ts account -> provider
│ ├── imap/ IMAP/SMTP: connections, search, threading, sending, actions
│ └── gmail/ optional Gmail API provider (OAuth)
├── tools/ the MCP tools (they know no provider)
└── cli/ admin scripts: users and tokens
tests/ unit tests (vitest), mirroring the layout of src/License
MIT — see LICENSE.
This server cannot be installed
Maintenance
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
- AlicenseNot gradedqualityDmaintenanceEnables reading and sending emails via IMAP and SMTP through the MCP protocol. Supports multiple email accounts and configuration via UI or environment variables.BSD 3-Clause
- AlicenseBqualityBmaintenanceEnables users to manage email accounts via IMAP/SMTP, including reading, searching, sending emails with attachments and calendar invites, all through natural language interactions with MCP-compatible clients.14MIT
- AlicenseAqualityBmaintenanceMCP server that enables email management (send, read, search, delete, etc.) via IMAP/SMTP, compatible with Gmail, Outlook, Yahoo, iCloud, and other standard mail servers.11MIT
- AlicenseNot gradedqualityAmaintenanceExposes any IMAP mailbox and SMTP relay as MCP tools, enabling email management (read, search, send, delete) through MCP-compatible agents.MIT
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.
Fully-managed email as MCP tools - register domains, real mailboxes, send and receive mail.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/HalloSouf/postbus-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server