Skip to main content
Glama
vivier

imap-mcp-server

by vivier
README.md
# imap-mcp-server

Minimal MCP server that bridges an MCP-capable agent to an IMAP4 mailbox. It uses
`fastmcp` with `imap-tools` to expose mailbox helpers so an agent can explore
folders, search, fetch message content, inspect and change message keywords,
decrypt PGP/MIME message bodies, and create draft replies without speaking IMAP
directly.

## Exposed tools
- `whoami()`: Return the configured login/email address.
- `list_mailboxes(directory, pattern)`: Enumerate folders using IMAP globbing.
- `mailboxes_status(directory)`: Return `MESSAGES`, `RECENT`, and `UNSEEN` counts.
- `search(directory="INBOX", criteria="ALL")`: Run an IMAP search and return matching UIDs.
- `get_header(directory, uids)`: Fetch raw headers (dict of header name to list of values) for each UID.
- `get_header_field(directory, uids, field)`: Fetch one header field for each UID.
- `get_text(directory, uids)`: Fetch the plain text body for each UID, decrypting PGP/MIME messages when possible.
- `get_html(directory, uids)`: Fetch the HTML body for each UID, decrypting PGP/MIME messages when possible.
- `get_size(directory, uids)`: Fetch RFC822 message sizes in bytes.
- `get_keywords(directory, uids)`: Fetch IMAP flags/keywords for each UID.
- `change_keywords(directory, uids, keywords, set)`: Add or remove IMAP flags/keywords.
- `create_message(content)`: Append a raw RFC 822 message to the `Drafts` mailbox.

Notes:
- UIDs are relative to the folder you query; use the same `directory` for `search` and subsequent `get_*` calls.
- Message fetches are read-only and avoid marking messages as seen.
- `get_text()` and `get_html()` use the local `gpg` keyring for `multipart/encrypted` messages with `application/pgp-encrypted`.
- `change_keywords()` and `create_message()` modify the mailbox state.

## Requirements
- Python 3.10+
- An IMAP account with host, username, and either:
  - Password/app password (traditional authentication), or
  - OAuth2 token (for Gmail and other OAuth-enabled providers)
- `gpg` installed on the host when you want to read PGP/MIME-encrypted messages

## Setup
1) (Recommended) `python3 -m venv .venv && source .venv/bin/activate`
2) Install dependencies: `python3 -m pip install -r requirements.txt`

## Configuration
The server reads credentials from environment variables; a `.env` file is supported via `python-dotenv`.

### Traditional IMAP Authentication
For standard IMAP servers:
```
IMAP_HOST=imap.example.com
IMAP_LOGIN=user@example.com
IMAP_PASSWORD=app-specific-password
```

### Gmail OAuth2 Authentication
For Gmail using OAuth2:
```
IMAP_HOST=imap.gmail.com
IMAP_LOGIN=user@gmail.com
IMAP_TOKEN=your-oauth2-access-token
```

### Optional GnuPG Configuration
To decrypt encrypted message bodies through `get_text()` and `get_html()`:
```
GNUPGHOME=/path/to/gpg-home
```

If `GNUPGHOME` is unset, the server uses the default GnuPG home directory for
the account running `mcp-server.py`. The required secret keys must already be
available in that keyring.

To obtain a Gmail OAuth2 token:
1) Create OAuth2 credentials in Google Cloud Console
   - Enable Gmail API for your project
   - Create OAuth 2.0 Client ID (Desktop application)
   - Note your client ID and client secret
2) Run the token helper script:
   ```
   python3 gmail_auth.py --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET
   ```
3) Follow the browser prompt to authorize access
4) Copy the `access_token` to your `.env` file as `IMAP_TOKEN`

Keep credentials out of version control and prefer app passwords or OAuth2 tokens when possible.

## Run

Direct execution: `python mcp-server.py`

Register with a client: `clause mcp add mailbox ./mcp-server.py` or `codex mcp add mailbox ./mcp-server.py`

You can also run the MCP server using http transport:

`fastmcp run mcp-server.py -t http --host 192.168.100.1`

And then add it to codex:

`codex mcp add email --url http://192.168.100.1:8000/mcp`

Ensure you run the command in a shell where the environment variables above are set or a `.env` file is present.

## Example

```text
> Review message-id 20260424091024.23495-1-deller@kernel.org from INBOX/Qemu/linux-user
```

1. High: `[PATCH 4/4]` mis-encodes successful `_NEW` `getsockopt()` results when
   userspace passes a buffer larger than `sizeof(struct target__kernel_sock_timeval)`.
   The new branch chooses the 64-bit copy path only on exact equality; any larger
   `len` falls back to `copy_to_user_timeval()`, which writes the old layout instead.
   The decision needs to be based on `optname`, with `len` clamped via `MIN(...)`,
   not on `len == sizeof(...)`. Relevant code is in `linux-user/syscall.c:2604`.
   Source for the patch hunk:
   <https://www.mail-archive.com/qemu-devel%40nongnu.org/msg1186189.html>

2. Medium: `[PATCH 1/4]` should not replace `abi_llong` with plain `int64_t` in
   `struct target__kernel_sock_timeval`. `abi_llong` carries target ABI alignment
   requirements; `int64_t` does not, so this can change guest-visible
   layout/alignment on some targets. The current struct is at
   `linux-user/syscall_defs.h:240`. Richard Henderson already NACKed this
   point in-thread:
   <https://www.mail-archive.com/qemu-devel%40nongnu.org/msg1186340.html>

I did not find another separate functional issue in patches 2/4 or 3/4 beyond
those. If you want, I can turn this into a reply mail in the repo's preferred
quoted format.

```text
> Write replies in Draft folder
```

Two draft replies are in `Drafts`:

- `Re: [PATCH 1/4] linux-user: Use int64_t in target__kernel_sock_timeval`
- `Re: [PATCH 4/4] linux-user: Add getsockopt() for SO_RCVTIMEO_NEW and SO_SNDTIMEO_NEW`

The first calls out the target ABI alignment issue with replacing `abi_llong`,
and the second points out the `_NEW getsockopt` bug when userspace provides a
buffer larger than the 64-bit `timeval` struct.

## License

Distributed under the GNU General Public License v2.0. See `LICENSE` for details.