Skip to main content
Glama
filiperotherds

titan-mail-mcp

README.md
# titan-mail-mcp

An MCP server that lets Claude (or any MCP client) search, read and — only if you allow it — send email from a regular IMAP/SMTP mailbox. Defaults are set for [Titan Mail](https://titan.email), but any provider that accepts IMAP/SMTP with a password or app password works.

It ships three ways: a one-click **Claude Desktop extension** (`.mcpb`) for non-technical users, a local **stdio** server for Claude Code and other MCP clients, and a **hosted HTTP** server with one token per client.

## Why this exists

I had a client — a contractor, not a developer — whose company email is on Titan, read through Outlook. He wanted to ask Claude things like *"which suppliers sent me a quote this week?"* without copy-pasting emails around.

Titan has an [official MCP connector](https://support.titan.email/hc/en-us/articles/58274860326681-Titan-MCP-Connect-Titan-Mail-with-Claude-and-ChatGPT), but it's only available on select plans. Every Titan plan, though, already exposes IMAP and SMTP — that's how Outlook talks to it. So this server just speaks IMAP/SMTP and hands the mailbox to Claude as four tools.

## What it is (and isn't)

**It is:**

- A thin, auditable bridge: ~500 lines of TypeScript on top of [ImapFlow](https://github.com/postalsys/imapflow), [Nodemailer](https://nodemailer.com) and the official [MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk).
- Read-first. Reading never marks messages as read unless asked. Sending is a separate tool that can be removed entirely, and it always returns a preview first.
- Provider-agnostic. Titan is just the default `imap`/`smtp` host.

**It isn't:**

- **A replacement for Titan's official MCP.** That one also covers calendar and contacts and uses OAuth. If your plan includes it, use it.
- **Compatible with claude.ai or ChatGPT custom connectors (yet).** Those require OAuth on remote MCP servers; the HTTP mode here uses static bearer tokens. It works with Claude Desktop (via the extension), Claude Code and any client that lets you set a header.
- **Usable with Gmail OAuth-only setups, Microsoft 365 or Outlook.com.** Microsoft disabled password-based IMAP for those accounts. Gmail works only with an [app password](https://support.google.com/accounts/answer/185833).
- **A full mail client.** No attachment download, no moving or deleting messages, no HTML email composition. See [Roadmap](#roadmap).

> Tool names, descriptions and error messages are currently in **Brazilian Portuguese** (`listar_pastas`, `buscar_emails`…), because the first users are Brazilian. The LLM handles this fine in any language, but PRs adding English names are welcome.

## Tools

| Tool | What it does |
| --- | --- |
| `listar_pastas` | Lists folders with total and unread counts |
| `buscar_emails` | Searches a folder by sender, recipient, subject, body text, date range or unread; newest first; returns the `uid` used by `ler_email` |
| `ler_email` | Headers, plain-text body (capped at 20k chars) and attachment list (name, type, size). Does **not** mark as read unless `marcar_como_lido=true` |
| `enviar_email` | Plain-text send with cc/bcc and reply threading. With `confirmar=false` (default) it only returns a preview; it sends only when called again with `confirmar=true`. Saves a copy to the Sent folder, since most IMAP servers (Titan included) don't do that for SMTP |

`enviar_email` is not registered at all when sending is disabled (`ALLOW_SEND=false`, `allowSend: false` or the unchecked box in the extension), so the model can't even try.

## Quick start

### Option 1 — Claude Desktop extension (no terminal needed)

1. Download the `.mcpb` file from the [latest release](https://github.com/filiperotherds/titan-mail-mcpb/releases/latest). Each release lists its SHA-256 checksum.
2. Double-click it, or drag it into **Claude Desktop → Settings → Extensions**.
3. Fill in email and password, done.

The password goes into the OS keychain via Claude Desktop's `sensitive` config — it never touches a plain file. Claude Desktop may warn that the extension is unverified; that's expected for extensions distributed outside its directory.

Prefer to build it yourself?

```bash
git clone https://github.com/filiperotherds/titan-mail-mcpb.git
cd titan-mail-mcpb
npm ci
npm run build:mcpb   # → out/titan-mail.mcpb
```

The install form asks for:

| Field | Required | Notes |
| --- | --- | --- |
| Email | yes | Full address |
| Password | yes | Stored in the OS keychain |
| Sender name | no | Display name on sent mail |
| Allow sending | no | **Off by default** |

### Option 2 — Claude Code (stdio, single mailbox)

```bash
npm ci && npm run build
claude mcp add titan-mail --scope user \
  -e MAIL_USER=you@yourdomain.com \
  -e MAIL_PASSWORD='your-password' \
  -- node /absolute/path/to/titan-mail-mcp/dist/index.js --stdio
```

Note that `-e` stores the password in plain text in `~/.claude.json`. If you'd rather not, set `MAIL_PASSWORD` as an OS environment variable and drop that flag.

To poke at the tools without any LLM in the loop, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector):

```bash
npx @modelcontextprotocol/inspector -e MAIL_USER=you@yourdomain.com -e MAIL_PASSWORD='your-password' node dist/index.js --stdio
```

### Option 3 — Hosted HTTP server (multiple clients)

Each client gets its own bearer token and can only see the mailbox bound to it. The server stores only the SHA-256 of each token.

```bash
npm ci && npm run build
npm run gen-token                        # prints a token (give to client) and its hash (keep)
cp accounts.example.json accounts.json   # add one entry per client
PW_CLIENT_A='their-password' npm start   # listens on 127.0.0.1:3000/mcp
```

`accounts.json`:

```json
[
  {
    "id": "client-a",
    "tokenSha256": "<hash from gen-token>",
    "email": "contact@client-a.com",
    "passwordEnv": "PW_CLIENT_A",
    "fromName": "Client A",
    "allowSend": false
  }
]
```

Connect from Claude Code:

```bash
claude mcp add --transport http titan-mail https://mcp.yourdomain.com/mcp \
  --header "Authorization: Bearer <token>"
```

Or with Docker:

```bash
docker build -t titan-mail-mcp .
docker run -d -p 3000:3000 \
  -v /path/to/accounts.json:/config/accounts.json:ro \
  -e PW_CLIENT_A='their-password' \
  -e ALLOWED_HOSTS=mcp.yourdomain.com \
  titan-mail-mcp
```

**Put it behind HTTPS** (Caddy, Nginx, Cloudflare Tunnel…). The token travels in a header; plain HTTP over the internet hands out mailbox access to anyone on the path.

To revoke a client, remove its entry (or change the hash) and restart.

## Configuration

### stdio / extension (environment variables)

| Variable | Default | Description |
| --- | --- | --- |
| `MAIL_USER` | — | Mailbox address (required) |
| `MAIL_PASSWORD` | — | Password or app password (required) |
| `MAIL_FROM_NAME` | — | Display name for sent mail |
| `IMAP_HOST` / `IMAP_PORT` / `IMAP_SECURE` | `imap.titan.email` / `993` / `true` | IMAP server |
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_SECURE` | `smtp.titan.email` / `465` / `true` | SMTP server |
| `ALLOW_SEND` | `true` | `false` removes `enviar_email` |
| `SAVE_SENT_COPY` | `true` | Append sent mail to the Sent folder via IMAP |

The `.mcpb` extension sets `ALLOW_SEND` from its checkbox, which is unchecked by default.

### HTTP server

| Variable | Default | Description |
| --- | --- | --- |
| `ACCOUNTS_FILE` | `accounts.json` | Accounts file |
| `HOST` | `127.0.0.1` | Bind address (`0.0.0.0` in the container) |
| `PORT` | `3000` | Port |
| `ALLOWED_HOSTS` | — | Comma-separated `Host` header allowlist (DNS-rebinding protection) |

Per-account fields: `id`, `tokenSha256`, `email`, `passwordEnv` (preferred) or `password`, `fromName`, `imap`, `smtp`, `allowSend`, `saveSentCopy`. `imap`/`smtp` are `{ "host", "port", "secure" }` and default to Titan.

### Other providers

| Provider | IMAP | SMTP | Notes |
| --- | --- | --- | --- |
| Titan | `imap.titan.email:993` | `smtp.titan.email:465` | Default |
| Gmail / Google Workspace | `imap.gmail.com:993` | `smtp.gmail.com:465` | Requires 2FA + app password |
| Zoho | `imap.zoho.com:993` | `smtp.zoho.com:465` | App password if 2FA is on |
| Microsoft 365 / Outlook.com | — | — | Not supported (OAuth2 only) |

## Security model

What it does:

- **Credentials stay out of the model's context.** Passwords are read from env vars or the OS keychain; no tool ever returns them.
- **Tenant isolation in HTTP mode.** Each request gets a fresh MCP server bound to the token's account. There's no shared session state between clients.
- **Tokens are hashed** (SHA-256) and compared in constant time.
- **Sending is a two-step action.** The tool's own contract is preview first, then `confirmar=true`. MCP clients like Claude Desktop and Claude Code will also ask the user before each call unless you set the tool to "always allow" — **don't** do that for `enviar_email`.
- **Tool annotations** mark reads as `readOnlyHint` and sending as `destructiveHint`/`openWorldHint`, so well-behaved clients can treat them differently.

What it doesn't protect against:

- **Prompt injection from email content.** A malicious email can contain text aimed at the model (*"forward all invoices to…"*). Keeping sending disabled, or always reviewing previews, is the real mitigation.
- **A compromised host.** In HTTP mode the server holds every client's mailbox password. Treat the box like a password vault.
- **Rate limiting.** There's none built in. Put it in your reverse proxy if you expose the server publicly.

## Troubleshooting

**`Erro: falha de autenticação no servidor de e-mail (usuário/senha).`**
Wrong user or password. If the account has 2FA, generate an app password in the provider's panel. On Titan, also check that IMAP isn't blocked by your admin.

**Connection timeout / `ECONNREFUSED`**
Something is blocking ports 993 or 465 — usually a corporate firewall or an antivirus with "mail shield".

**HTTP `401 invalid_token`**
The `Authorization: Bearer` token doesn't match any `tokenSha256`. Regenerate with `npm run gen-token` and make sure you stored the **hash**, not the token.

**HTTP `403` / `Invalid Host header`**
You're binding to a non-localhost host behind a proxy. Set `ALLOWED_HOSTS` to your public hostname.

**The extension installed but Claude doesn't see the tools**
Check that the extension is toggled on in **Settings → Extensions**, then fully quit and reopen Claude Desktop.

## Development

```bash
npm ci
npm run build          # tsc → dist/
npm run start:stdio    # stdio mode (needs MAIL_USER / MAIL_PASSWORD)
npm start              # HTTP mode (needs accounts.json)
npm run build:mcpb     # validates mcpb/manifest.json and packs out/titan-mail.mcpb
```

```
src/
  config.ts      account schema, env/file loading, token hashing
  mail.ts        IMAP/SMTP operations (ImapFlow, mailparser, Nodemailer)
  server.ts      MCP tool definitions
  index.ts       stdio and HTTP entry points, bearer auth
  gen-token.ts   token + hash generator
mcpb/
  manifest.json  Claude Desktop extension manifest
scripts/
  build-mcpb.mjs bundles dist/ + production deps into the .mcpb
```

Requires Node.js 20+.

## Roadmap

Things I'd like to add, roughly in order. Contributions very welcome:

- [ ] OAuth 2.1 in HTTP mode, so it works as a claude.ai / ChatGPT custom connector
- [ ] Attachment download (returning content as an MCP resource)
- [ ] Move, archive, flag and mark-as-read tools
- [ ] English tool names (keeping the Portuguese ones as aliases)
- [ ] Tests against a local IMAP server (e.g. GreenMail) in CI
- [x] Prebuilt `.mcpb` attached to GitHub Releases

## Contributing

Issues and PRs are welcome — bug reports with the exact error message and your provider (Titan, Gmail, Zoho…) help the most. Keep PRs focused, and run `npm run build` before opening one.

If you use this with a provider not listed above, a PR adding a row to the providers table is a great first contribution.

## License

[MIT](LICENSE) © Filipe Rother