https://github.com/mitigate-dev/mail_mcp
https://github.com/mitigate-dev/mail_mcp
Last synced: 2 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/mitigate-dev/mail_mcp
- Owner: mitigate-dev
- License: mit
- Created: 2026-04-27T08:49:48.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-27T15:03:27.000Z (2 months ago)
- Last Synced: 2026-04-27T17:06:14.142Z (2 months ago)
- Language: Ruby
- Size: 82 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Mail MCP
A hosted [Model Context Protocol](https://modelcontextprotocol.io/) server for IMAP and SMTP email, built in Ruby. It acts as both an OAuth 2.1 Authorization Server and an MCP Resource Server.
## Architecture
```
Client (Claude Desktop / MCP Inspector)
│
├─ OAuth 2.1 flow
│ GET /.well-known/oauth-protected-resource RFC 9728 metadata
│ GET /.well-known/oauth-authorization-server RFC 8414 metadata
│ GET /oauth/authorize Login UI
│ POST /oauth/authorize Validate IMAP/SMTP → issue code
│ POST /oauth/token Code + PKCE + client_secret → tokens
│ (also: refresh_token grant)
│
└─ MCP calls (all HTTP methods)
/mcp Bearer access token → decrypt creds → IMAP/SMTP operations
```
### How authentication works
Clients are provisioned once via the `bin/mail_mcp generate` CLI, which produces:
- **`client_id`** — a JWE token (encrypted, opaque) encoding the IMAP/SMTP server configuration and the `client_secret`. Only the server can decrypt it.
- **`client_secret`** — a random secret used to authenticate the client at the token endpoint.
The OAuth flow:
1. Client redirects the user to `/oauth/authorize?client_id=&...`
2. Server decrypts the `client_id` JWE to learn which IMAP/SMTP servers to connect to
3. User enters their IMAP/SMTP username and password in the login form
4. Server validates both connections live; shows an error banner on failure
5. On success: credentials are encrypted directly in a JWE access token and a JWE refresh token
6. Client exchanges the authorization code (`POST /oauth/token`) with `client_id` + `client_secret`; receives `access_token` + `refresh_token`
7. Every MCP request carries `Authorization: Bearer `; the server decrypts credentials per-request

### Token formats
All tokens are **5-part JWE** (`dir` / `A256GCM`), encrypted with `ENCRYPTION_KEY`. There is no separate signing key.
| Token | `typ` claim | Expiry | Contents |
|---------------|-------------|---------|--------------------------------------|
| `client_id` | `client_id` | none | imap/smtp host+port, `client_secret` |
| Access token | `access` | 8 hours | IMAP + SMTP credentials |
| Refresh token | `refresh` | 30 days | IMAP + SMTP credentials |
## Directory Structure
```
mail_mcp/
├── Gemfile
├── .env.sample # Environment variable template
├── bin/
│ └── mail_mcp # CLI: `generate` (client_id) and `server` (puma)
├── config.ru # Rack entry point — run MailMCP::App.new
├── config/
│ └── puma.rb # Puma config (single worker, 5 threads)
├── lib/
│ ├── mail_mcp.rb # Module root + requires
│ └── mail_mcp/
│ ├── jwt_service.rb # All JWE tokens (access, refresh, client_id)
│ ├── pkce.rb # PKCE S256 challenge/verify
│ ├── credential_context.rb # Struct passed as MCP server_context per request
│ ├── imap_client.rb # net-imap wrapper
│ ├── smtp_client.rb # net-smtp wrapper
│ ├── attachment_store.rb # S3 upload + presigned URLs (7 days)
│ ├── tool.rb # MailMCP::Tool base class
│ ├── app.rb # Sinatra: OAuth + MCP /mcp route (all methods)
│ └── tools/*.rb # MCP tool classes
├── views/
│ └── login.erb # Login form (username + password only)
├── spec/ # RSpec test suite
└── Dockerfile
```
## Configuration
Copy `.env.sample` to `.env` and fill in the values:
| Variable | Description |
|-------------------------|---------------------------------------------------------------------|
| `BASE_URL` | Public URL of this server, e.g. `https://mail.mcp.example.com` |
| `ENCRYPTION_KEY` | AES-256 key (base64-encoded 32 bytes) — used for **all** JWE tokens |
| `AWS_ACCESS_KEY_ID` | AWS credentials for S3 attachment storage |
| `AWS_SECRET_ACCESS_KEY` | AWS credentials for S3 attachment storage |
| `AWS_REGION` | S3 bucket region, e.g. `us-east-1` |
| `AWS_S3_BUCKET` | S3 bucket name for attachments |
| `PORT` | HTTP port (default `3000`) |
| `RACK_ENV` | `development` or `production` |
| `MAIL_MCP_LOG_LEVEL` | Log level (`DEBUG`, `INFO`, `WARN`, `ERROR`, default `INFO`) |
Generate `ENCRYPTION_KEY`:
```bash
ruby -e "require 'base64','securerandom'; puts Base64.strict_encode64(SecureRandom.bytes(32))"
```
IMAP/SMTP host and port are embedded in the `client_id` JWE and are never passed as headers or query parameters.
## Setup
```bash
# Install dependencies
bundle install
# Copy and edit environment variables
cp .env.sample .env
$EDITOR .env
# Run tests
bundle exec rspec
# Start the server
bundle exec puma -C config/puma.rb
```
## Provisioning a Client
Run `bin/mail_mcp generate` once per mail server configuration. The resulting `client_id` and `client_secret` are configured in the MCP client (e.g. Claude Desktop).
```bash
bundle exec bin/mail_mcp generate \
--imap-host=imap.gmail.com \
--imap-port=993 \
--smtp-host=smtp.gmail.com \
--smtp-port=465
# Client ID: eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0...
# Client Secret: 713576e2f94802b9d9abfd755e38e29b63e491df...
#
# IMAP: imap.gmail.com:993 (ssl=true)
# SMTP: smtp.gmail.com:465 (ssl=true)
```
| Flag | Default | Description |
|--------------------|----------------------|-------------------------|
| `--imap-host=HOST` | required | IMAP server hostname |
| `--imap-port=PORT` | `993` | IMAP port |
| `--[no-]imap-ssl` | `true` when port 993 | Enable SSL/TLS for IMAP |
| `--smtp-host=HOST` | required | SMTP server hostname |
| `--smtp-port=PORT` | `465` | SMTP port |
| `--[no-]smtp-ssl` | `true` when port 465 | Enable SSL/TLS for SMTP |
## OAuth 2.1 Flow
1. **Discovery** — client fetches `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`
2. **Authorization** — client redirects user to `/oauth/authorize?client_id=&code_challenge=&...`
3. **Login** — server decrypts `client_id` JWE to get IMAP/SMTP hosts; user enters credentials; server validates both connections live
4. **Code exchange** — `POST /oauth/token` with `grant_type=authorization_code`, `code`, `code_verifier`, `client_id`, `client_secret`; server issues `access_token` + `refresh_token`
5. **Token refresh** — `POST /oauth/token` with `grant_type=refresh_token`, `refresh_token`, `client_id`, `client_secret`; server issues a new `access_token` + `refresh_token`
6. **MCP calls** — client sends `Authorization: Bearer ` on every request; server decrypts credentials per-request via a stateless per-request MCP server
## MCP Tools
| Tool | Parameters | Description |
|-----------------------------|-------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|
| `list_mailboxes` | — | List all IMAP folders |
| `list_mail_messages` | `folder`, `page`, `per_page` | List messages with pagination |
| `get_mail_message` | `folder`, `uid` | Fetch full message; attachments uploaded to S3 and returned as presigned URLs |
| `search_mail_messages` | `folder`, `query` | Raw IMAP SEARCH criteria, e.g. `UNSEEN` or `FROM alice@example.com SINCE 01-Jan-2025` |
| `send_mail_message` | `to`, `subject`, `text_body`, `cc`, `bcc`, `html_body`, `attachment_urls`, `folder` | Send via SMTP and append to the Sent folder via IMAP; attachments fetched from S3 presigned URLs |
| `create_draft_mail_message` | `to`, `subject`, `text_body`, `cc`, `bcc`, `html_body`, `attachment_urls`, `folder` | Append to Drafts via IMAP APPEND; attachments fetched from S3 presigned URLs |
| `delete_mail_message` | `folder`, `uid` | Mark `\Deleted` + EXPUNGE |
| `move_mail_message` | `folder`, `uid`, `destination` | IMAP MOVE (or COPY+DELETE fallback) |
| `update_mail_message_flags` | `folder`, `uid`, `add`, `remove` | Add/remove IMAP flags, e.g. `\Seen`, `\Flagged` |
Attachments are never returned as binary data — they are uploaded to S3 on first access and returned as presigned URLs valid for 7 days.
## Docker
```bash
docker build -t mail_mcp .
docker run -p 3000:3000 --env-file .env mail_mcp
```