imap-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- imap_mcp-0.1.0/.gitignore +8 -0
- imap_mcp-0.1.0/CHANGELOG.md +40 -0
- imap_mcp-0.1.0/LICENSE +21 -0
- imap_mcp-0.1.0/PKG-INFO +197 -0
- imap_mcp-0.1.0/README.md +167 -0
- imap_mcp-0.1.0/pyproject.toml +109 -0
- imap_mcp-0.1.0/scripts/changelog_section.py +63 -0
- imap_mcp-0.1.0/scripts/check_licenses.py +35 -0
- imap_mcp-0.1.0/src/imap_mcp/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/clock.py +18 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/filesystem.py +89 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/flock.py +76 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/imap/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/imap/guard.py +172 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/imap/session.py +392 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/imap/structure.py +155 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/imap/uidset.py +64 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/keychain.py +125 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/sqlite/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/sqlite/connection.py +136 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/sqlite/fts.py +36 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/sqlite/index.py +700 -0
- imap_mcp-0.1.0/src/imap_mcp/adapters/sqlite/schema.py +185 -0
- imap_mcp-0.1.0/src/imap_mcp/bootstrap.py +143 -0
- imap_mcp-0.1.0/src/imap_mcp/config.py +115 -0
- imap_mcp-0.1.0/src/imap_mcp/core/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/core/attachments.py +141 -0
- imap_mcp-0.1.0/src/imap_mcp/core/drafts.py +191 -0
- imap_mcp-0.1.0/src/imap_mcp/core/search.py +84 -0
- imap_mcp-0.1.0/src/imap_mcp/core/services.py +25 -0
- imap_mcp-0.1.0/src/imap_mcp/core/sync.py +512 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/content/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/content/auth.py +110 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/content/html.py +119 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/content/identity.py +44 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/content/parse.py +299 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/content/sanitize.py +52 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/enums.py +51 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/errors.py +42 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/folders.py +63 -0
- imap_mcp-0.1.0/src/imap_mcp/domain/model.py +337 -0
- imap_mcp-0.1.0/src/imap_mcp/entrypoints/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/entrypoints/cli.py +229 -0
- imap_mcp-0.1.0/src/imap_mcp/entrypoints/mcp/__init__.py +1 -0
- imap_mcp-0.1.0/src/imap_mcp/entrypoints/mcp/app.py +236 -0
- imap_mcp-0.1.0/src/imap_mcp/entrypoints/mcp/views.py +395 -0
- imap_mcp-0.1.0/src/imap_mcp/log.py +69 -0
- imap_mcp-0.1.0/src/imap_mcp/ports.py +274 -0
- imap_mcp-0.1.0/tests/conftest.py +76 -0
- imap_mcp-0.1.0/tests/factories.py +84 -0
- imap_mcp-0.1.0/tests/fakes.py +83 -0
- imap_mcp-0.1.0/tests/imapserver/server.py +768 -0
- imap_mcp-0.1.0/tests/integration/conftest.py +56 -0
- imap_mcp-0.1.0/tests/integration/test_hostile_mail.py +144 -0
- imap_mcp-0.1.0/tests/integration/test_scenarios.py +210 -0
- imap_mcp-0.1.0/tests/lockholder.py +19 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/adapters/imap/test_guard.py +104 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/adapters/imap/test_session.py +244 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/adapters/imap/test_structure.py +87 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/adapters/imap/test_uidset.py +25 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/adapters/sqlite/test_index.py +312 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/adapters/test_filesystem.py +78 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/adapters/test_flock.py +48 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/adapters/test_keychain.py +79 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/core/conftest.py +79 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/core/test_sync.py +489 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/domain/content/test_auth.py +100 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/domain/content/test_html.py +99 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/domain/content/test_parse.py +158 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/domain/content/test_sanitize.py +11 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/entrypoints/test_cli.py +105 -0
- imap_mcp-0.1.0/tests/unit/imap_mcp/test_config.py +93 -0
- imap_mcp-0.1.0/tests/unit/test_architecture.py +64 -0
- imap_mcp-0.1.0/tests/unit/test_changelog_section.py +60 -0
- imap_mcp-0.1.0/uv.lock +1299 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- MCP server over stdio with six tools: `search_mail`, `get_message`, `list_folders`, `sync_now`, `download_attachment` and `create_draft`.
|
|
15
|
+
- Local SQLite index with ranked FTS5 keyword search. Search skips Junk and Trash unless the agent names the folder.
|
|
16
|
+
- Sync on a tool call when the data is more than 15 minutes old, with a 20 second time limit, and `sync_now` to force one. Every result reports how old the data is.
|
|
17
|
+
- Incremental sync: `STATUS` checks, `CONDSTORE` flag updates, moves linked without a download, and `UIDVALIDITY` relinks that resume after an interruption.
|
|
18
|
+
- A cross-process sync lock, so several agent sessions can share one index.
|
|
19
|
+
- An IMAP command allowlist that refuses every write except `APPEND` to the Drafts folder.
|
|
20
|
+
- Prompt-injection defences: sender text kept apart from server data, nonce-tagged message text, hidden HTML and invisible Unicode removed, and sender authentication results.
|
|
21
|
+
- Markdown drafts with raw HTML off and images rendered as links.
|
|
22
|
+
- Attachment download that never overwrites a file and sets the macOS quarantine attribute.
|
|
23
|
+
- The macOS Keychain as the password store.
|
|
24
|
+
- CLI commands: `serve`, `set-password`, `probe`, `sync`, `status`, `vacuum` and `purge`.
|
|
25
|
+
- The `auth_servers` setting names the domains whose `Authentication-Results` headers count.
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
- Authentication verdicts come only from the top headers whose authserv-id is in `auth_servers`, and only from the `method=result` token at the start of each clause. Sender text in a comment or in `smtp.mailfrom` can no longer add a pass verdict.
|
|
30
|
+
- The message identity includes the RFC822 size, so a forged copy with a real message's `Message-ID`, `Date` and `From` no longer merges into it. Schema v3 clears the index, and the next sync downloads all mail again.
|
|
31
|
+
- A message the client cannot parse, such as one nested hundreds of MIME levels deep, is skipped and the sync reports `partial`. It no longer stops the sync or makes every read tool fail.
|
|
32
|
+
- IMAP error messages no longer contain library error text, which could quote headers into logs and the `sync.error` field.
|
|
33
|
+
- `attachments[].type` shows only known media types.
|
|
34
|
+
- `create_draft` and `download_attachment` carry `openWorldHint: true`.
|
|
35
|
+
- Draft headers hold the parsed address that `new_recipients` reports. A non-ASCII address is refused as invalid.
|
|
36
|
+
- Attachment names are capped at 255 UTF-8 bytes.
|
|
37
|
+
- CI actions are pinned to commit SHAs, and uv and hatchling to exact versions.
|
|
38
|
+
|
|
39
|
+
[unreleased]: https://github.com/bcrisp4/imap-mcp/compare/v0.1.0...HEAD
|
|
40
|
+
[0.1.0]: https://github.com/bcrisp4/imap-mcp/releases/tag/v0.1.0
|
imap_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Crisp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
imap_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: imap-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local, read-only MCP server with keyword search over one IMAP mailbox
|
|
5
|
+
Project-URL: Homepage, https://github.com/bcrisp4/imap-mcp
|
|
6
|
+
Project-URL: Source, https://github.com/bcrisp4/imap-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/bcrisp4/imap-mcp/issues
|
|
8
|
+
Project-URL: Design, https://github.com/bcrisp4/imap-mcp/blob/main/docs/designs/imap-mcp.md
|
|
9
|
+
Project-URL: Changelog, https://github.com/bcrisp4/imap-mcp/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Ben Crisp <ben@thecrisp.io>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: email,icloud,imap,mcp,model-context-protocol
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Communications :: Email :: Post-Office :: IMAP
|
|
21
|
+
Requires-Python: >=3.14
|
|
22
|
+
Requires-Dist: beautifulsoup4>=4.15
|
|
23
|
+
Requires-Dist: imapclient>=4.1
|
|
24
|
+
Requires-Dist: lxml>=6.1
|
|
25
|
+
Requires-Dist: markdown-it-py>=4.2
|
|
26
|
+
Requires-Dist: mcp>=2.3
|
|
27
|
+
Requires-Dist: pydantic>=2.14
|
|
28
|
+
Requires-Dist: structlog>=26.1
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# imap-mcp
|
|
32
|
+
|
|
33
|
+
imap-mcp is a local MCP server that gives an AI agent read-only, searchable access to one IMAP mailbox. It can write drafts and can never send mail.
|
|
34
|
+
|
|
35
|
+
It syncs the mailbox into a local SQLite index with ranked keyword search. It syncs again on a tool call when the data is more than 15 minutes old, and every result tells the agent how old the data is. iCloud is the main target. Any IMAP server that accepts an app password also works.
|
|
36
|
+
|
|
37
|
+
Status: alpha. Tool names and parameters can change before 1.0.0. See the [changelog](https://github.com/bcrisp4/imap-mcp/blob/main/CHANGELOG.md).
|
|
38
|
+
|
|
39
|
+
## What it never does
|
|
40
|
+
|
|
41
|
+
- It never sends mail. The package contains no SMTP code.
|
|
42
|
+
- It never deletes, moves or flags mail. A read never marks a message as read. A command allowlist checks every IMAP command before it goes on the wire.
|
|
43
|
+
- It never listens on the network. It runs on stdio, started by your agent.
|
|
44
|
+
|
|
45
|
+
Email is untrusted input. Results keep sender text apart from server data, remove hidden HTML and invisible Unicode, and show sender authentication results. See [Security](https://github.com/bcrisp4/imap-mcp/blob/main/docs/designs/imap-mcp.md#security) in the design doc.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- macOS. The password lives in the macOS Keychain.
|
|
50
|
+
- [uv](https://docs.astral.sh/uv/). It installs Python 3.14 if you do not have it.
|
|
51
|
+
- An IMAP account with an app password. For iCloud, create one at [account.apple.com](https://account.apple.com) under Sign-In and Security, App-Specific Passwords.
|
|
52
|
+
|
|
53
|
+
## Set up
|
|
54
|
+
|
|
55
|
+
1. Create `~/.config/imap-mcp/config.toml`. For iCloud, `username` is your Apple Account email or a custom-domain address on the account:
|
|
56
|
+
|
|
57
|
+
```toml
|
|
58
|
+
host = "imap.mail.me.com"
|
|
59
|
+
port = 993
|
|
60
|
+
username = "you@icloud.com"
|
|
61
|
+
from_address = "you@icloud.com"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
2. Store the app password in the Keychain. `security` prompts for it twice. When macOS asks for Keychain access later, choose Always Allow for `/usr/bin/security`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uvx imap-mcp set-password
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
3. Check the connection. This prints the server capabilities and folders, and no message content:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uvx imap-mcp probe
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
4. Run the first full sync. A large mailbox takes a long time, because iCloud limits download speed. You can stop it and run it again later. It continues where it stopped:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
uvx imap-mcp sync
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Optional settings
|
|
83
|
+
|
|
84
|
+
| Key | Default | Effect |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `download_dir` | `~/Downloads` | Where `download_attachment` saves files |
|
|
87
|
+
| `sync_interval_minutes` | `15` | How old the data can be before a tool call syncs |
|
|
88
|
+
| `sync_budget_seconds` | `20` | The time limit of a sync that a tool call starts |
|
|
89
|
+
| `db_path` | `~/Library/Application Support/imap-mcp/imap-mcp.db` | The index. It must be on a local disk, not in iCloud Drive. |
|
|
90
|
+
| `drafts_folder` | empty | The Drafts folder name. Empty means find it by role or name. |
|
|
91
|
+
| `exclude_folders` | `[]` | Folders never to sync |
|
|
92
|
+
| `auth_servers` | `["icloud.com"]` | Domains of your IMAP server's `Authentication-Results` headers. Only those headers give the `spf`, `dkim` and `dmarc` verdicts. |
|
|
93
|
+
|
|
94
|
+
`IMAP_MCP_CONFIG` overrides the config file path.
|
|
95
|
+
|
|
96
|
+
## Connect the Claude desktop app
|
|
97
|
+
|
|
98
|
+
1. Find the absolute path of `uvx`. The desktop app starts servers with a minimal `PATH`, so a bare `uvx` fails. Do not use a version-manager shim:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
which -a uvx
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
2. Open Claude, then Settings, Developer, Edit Config. This opens `~/Library/Application Support/Claude/claude_desktop_config.json`.
|
|
105
|
+
|
|
106
|
+
3. Add the server, with your `uvx` path:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"imap": {
|
|
112
|
+
"command": "/opt/homebrew/bin/uvx",
|
|
113
|
+
"args": ["imap-mcp@latest", "serve"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`imap-mcp@latest` checks for a new release at each start. Use a fixed version such as `imap-mcp@0.1.0` to upgrade only when you choose.
|
|
120
|
+
|
|
121
|
+
4. Quit and restart Claude. The tools appear under the imap server in the tools menu.
|
|
122
|
+
|
|
123
|
+
5. Keep `create_draft` on "Ask" when Claude asks for permission. Do not choose "Always allow".
|
|
124
|
+
|
|
125
|
+
Server logs go to `~/Library/Logs/Claude/mcp-server-imap.log`. Logs never contain message content or addresses.
|
|
126
|
+
|
|
127
|
+
## Connect Claude Code or another MCP client
|
|
128
|
+
|
|
129
|
+
Any client that runs a stdio server can use the same command:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
claude mcp add imap -- uvx imap-mcp@latest serve
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Usage
|
|
136
|
+
|
|
137
|
+
Ask the agent in plain language. It picks the tools. For example:
|
|
138
|
+
|
|
139
|
+
- "When does my car insurance renew?" The agent searches, reads the best match and answers with the date and the sender.
|
|
140
|
+
- "What did the plumber say about Thursday? Draft a reply that accepts 9am." The agent finds the thread and writes a draft. You open Drafts in Mail, edit it and send it yourself.
|
|
141
|
+
- "Save the PDF invoice from Acme." The agent saves it to `~/Downloads` and tells you the path.
|
|
142
|
+
- "Check for new mail first." The agent calls `sync_now`.
|
|
143
|
+
|
|
144
|
+
### Tools
|
|
145
|
+
|
|
146
|
+
| Tool | Effect |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `search_mail` | Ranked keyword search with filters (folder, sender, recipient, dates, unread, flagged, attachments). Skips Junk and Trash unless named. |
|
|
149
|
+
| `get_message` | One message: headers, plain-text body, attachment list |
|
|
150
|
+
| `list_folders` | Folders with message and unread counts |
|
|
151
|
+
| `sync_now` | Sync now, within the time limit |
|
|
152
|
+
| `download_attachment` | Save one attachment to `download_dir` |
|
|
153
|
+
| `create_draft` | Write a Markdown draft to the Drafts folder. Never sends. |
|
|
154
|
+
|
|
155
|
+
### Commands
|
|
156
|
+
|
|
157
|
+
| Command | Effect |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `imap-mcp serve` | Run the MCP server on stdio. Your agent runs this. |
|
|
160
|
+
| `imap-mcp set-password` | Store the app password in the Keychain |
|
|
161
|
+
| `imap-mcp probe` | Log in and print capabilities and folders |
|
|
162
|
+
| `imap-mcp sync` | Run a full sync with no time limit |
|
|
163
|
+
| `imap-mcp status` | Show sync state, folder counts and database size |
|
|
164
|
+
| `imap-mcp vacuum` | Rebuild the database file |
|
|
165
|
+
| `imap-mcp purge --yes` | Delete the index. The next sync rebuilds it. |
|
|
166
|
+
|
|
167
|
+
## Prompt injection
|
|
168
|
+
|
|
169
|
+
An email can contain text written to steer the agent. imap-mcp cannot send mail, so the worst it can do on its own is write a draft. But if the same agent also has a tool that can reach the network (web fetch, HTTP, another mail tool), a hostile email can tell the agent to leak data through that tool. In a session that reads mail, give the agent as few network tools as possible.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
git clone https://github.com/bcrisp4/imap-mcp
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
cd imap-mcp && uv sync
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
uv run ruff check && uv run ruff format --check && uv run pyright && uv run mypy && uv run pytest
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
To run your checkout instead of the release, use `uv run --directory /path/to/imap-mcp imap-mcp serve` as the server command.
|
|
186
|
+
|
|
187
|
+
Tests never connect to a real mailbox. They use a TLS fake IMAP server that records every command and reproduces the iCloud quirks. Read [AGENTS.md](https://github.com/bcrisp4/imap-mcp/blob/main/AGENTS.md) before you change code, and [docs/releasing.md](https://github.com/bcrisp4/imap-mcp/blob/main/docs/releasing.md) to make a release.
|
|
188
|
+
|
|
189
|
+
## Docs
|
|
190
|
+
|
|
191
|
+
- [Design](https://github.com/bcrisp4/imap-mcp/blob/main/docs/designs/imap-mcp.md): goals, security model, sync design and decisions
|
|
192
|
+
- [Changelog](https://github.com/bcrisp4/imap-mcp/blob/main/CHANGELOG.md)
|
|
193
|
+
- API reference: run `uv run pdoc imap_mcp` in a clone
|
|
194
|
+
|
|
195
|
+
## Licence
|
|
196
|
+
|
|
197
|
+
MIT. See [LICENSE](https://github.com/bcrisp4/imap-mcp/blob/main/LICENSE).
|
imap_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# imap-mcp
|
|
2
|
+
|
|
3
|
+
imap-mcp is a local MCP server that gives an AI agent read-only, searchable access to one IMAP mailbox. It can write drafts and can never send mail.
|
|
4
|
+
|
|
5
|
+
It syncs the mailbox into a local SQLite index with ranked keyword search. It syncs again on a tool call when the data is more than 15 minutes old, and every result tells the agent how old the data is. iCloud is the main target. Any IMAP server that accepts an app password also works.
|
|
6
|
+
|
|
7
|
+
Status: alpha. Tool names and parameters can change before 1.0.0. See the [changelog](https://github.com/bcrisp4/imap-mcp/blob/main/CHANGELOG.md).
|
|
8
|
+
|
|
9
|
+
## What it never does
|
|
10
|
+
|
|
11
|
+
- It never sends mail. The package contains no SMTP code.
|
|
12
|
+
- It never deletes, moves or flags mail. A read never marks a message as read. A command allowlist checks every IMAP command before it goes on the wire.
|
|
13
|
+
- It never listens on the network. It runs on stdio, started by your agent.
|
|
14
|
+
|
|
15
|
+
Email is untrusted input. Results keep sender text apart from server data, remove hidden HTML and invisible Unicode, and show sender authentication results. See [Security](https://github.com/bcrisp4/imap-mcp/blob/main/docs/designs/imap-mcp.md#security) in the design doc.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- macOS. The password lives in the macOS Keychain.
|
|
20
|
+
- [uv](https://docs.astral.sh/uv/). It installs Python 3.14 if you do not have it.
|
|
21
|
+
- An IMAP account with an app password. For iCloud, create one at [account.apple.com](https://account.apple.com) under Sign-In and Security, App-Specific Passwords.
|
|
22
|
+
|
|
23
|
+
## Set up
|
|
24
|
+
|
|
25
|
+
1. Create `~/.config/imap-mcp/config.toml`. For iCloud, `username` is your Apple Account email or a custom-domain address on the account:
|
|
26
|
+
|
|
27
|
+
```toml
|
|
28
|
+
host = "imap.mail.me.com"
|
|
29
|
+
port = 993
|
|
30
|
+
username = "you@icloud.com"
|
|
31
|
+
from_address = "you@icloud.com"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
2. Store the app password in the Keychain. `security` prompts for it twice. When macOS asks for Keychain access later, choose Always Allow for `/usr/bin/security`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uvx imap-mcp set-password
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
3. Check the connection. This prints the server capabilities and folders, and no message content:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uvx imap-mcp probe
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
4. Run the first full sync. A large mailbox takes a long time, because iCloud limits download speed. You can stop it and run it again later. It continues where it stopped:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uvx imap-mcp sync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Optional settings
|
|
53
|
+
|
|
54
|
+
| Key | Default | Effect |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `download_dir` | `~/Downloads` | Where `download_attachment` saves files |
|
|
57
|
+
| `sync_interval_minutes` | `15` | How old the data can be before a tool call syncs |
|
|
58
|
+
| `sync_budget_seconds` | `20` | The time limit of a sync that a tool call starts |
|
|
59
|
+
| `db_path` | `~/Library/Application Support/imap-mcp/imap-mcp.db` | The index. It must be on a local disk, not in iCloud Drive. |
|
|
60
|
+
| `drafts_folder` | empty | The Drafts folder name. Empty means find it by role or name. |
|
|
61
|
+
| `exclude_folders` | `[]` | Folders never to sync |
|
|
62
|
+
| `auth_servers` | `["icloud.com"]` | Domains of your IMAP server's `Authentication-Results` headers. Only those headers give the `spf`, `dkim` and `dmarc` verdicts. |
|
|
63
|
+
|
|
64
|
+
`IMAP_MCP_CONFIG` overrides the config file path.
|
|
65
|
+
|
|
66
|
+
## Connect the Claude desktop app
|
|
67
|
+
|
|
68
|
+
1. Find the absolute path of `uvx`. The desktop app starts servers with a minimal `PATH`, so a bare `uvx` fails. Do not use a version-manager shim:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
which -a uvx
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
2. Open Claude, then Settings, Developer, Edit Config. This opens `~/Library/Application Support/Claude/claude_desktop_config.json`.
|
|
75
|
+
|
|
76
|
+
3. Add the server, with your `uvx` path:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"imap": {
|
|
82
|
+
"command": "/opt/homebrew/bin/uvx",
|
|
83
|
+
"args": ["imap-mcp@latest", "serve"]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`imap-mcp@latest` checks for a new release at each start. Use a fixed version such as `imap-mcp@0.1.0` to upgrade only when you choose.
|
|
90
|
+
|
|
91
|
+
4. Quit and restart Claude. The tools appear under the imap server in the tools menu.
|
|
92
|
+
|
|
93
|
+
5. Keep `create_draft` on "Ask" when Claude asks for permission. Do not choose "Always allow".
|
|
94
|
+
|
|
95
|
+
Server logs go to `~/Library/Logs/Claude/mcp-server-imap.log`. Logs never contain message content or addresses.
|
|
96
|
+
|
|
97
|
+
## Connect Claude Code or another MCP client
|
|
98
|
+
|
|
99
|
+
Any client that runs a stdio server can use the same command:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
claude mcp add imap -- uvx imap-mcp@latest serve
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
Ask the agent in plain language. It picks the tools. For example:
|
|
108
|
+
|
|
109
|
+
- "When does my car insurance renew?" The agent searches, reads the best match and answers with the date and the sender.
|
|
110
|
+
- "What did the plumber say about Thursday? Draft a reply that accepts 9am." The agent finds the thread and writes a draft. You open Drafts in Mail, edit it and send it yourself.
|
|
111
|
+
- "Save the PDF invoice from Acme." The agent saves it to `~/Downloads` and tells you the path.
|
|
112
|
+
- "Check for new mail first." The agent calls `sync_now`.
|
|
113
|
+
|
|
114
|
+
### Tools
|
|
115
|
+
|
|
116
|
+
| Tool | Effect |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `search_mail` | Ranked keyword search with filters (folder, sender, recipient, dates, unread, flagged, attachments). Skips Junk and Trash unless named. |
|
|
119
|
+
| `get_message` | One message: headers, plain-text body, attachment list |
|
|
120
|
+
| `list_folders` | Folders with message and unread counts |
|
|
121
|
+
| `sync_now` | Sync now, within the time limit |
|
|
122
|
+
| `download_attachment` | Save one attachment to `download_dir` |
|
|
123
|
+
| `create_draft` | Write a Markdown draft to the Drafts folder. Never sends. |
|
|
124
|
+
|
|
125
|
+
### Commands
|
|
126
|
+
|
|
127
|
+
| Command | Effect |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `imap-mcp serve` | Run the MCP server on stdio. Your agent runs this. |
|
|
130
|
+
| `imap-mcp set-password` | Store the app password in the Keychain |
|
|
131
|
+
| `imap-mcp probe` | Log in and print capabilities and folders |
|
|
132
|
+
| `imap-mcp sync` | Run a full sync with no time limit |
|
|
133
|
+
| `imap-mcp status` | Show sync state, folder counts and database size |
|
|
134
|
+
| `imap-mcp vacuum` | Rebuild the database file |
|
|
135
|
+
| `imap-mcp purge --yes` | Delete the index. The next sync rebuilds it. |
|
|
136
|
+
|
|
137
|
+
## Prompt injection
|
|
138
|
+
|
|
139
|
+
An email can contain text written to steer the agent. imap-mcp cannot send mail, so the worst it can do on its own is write a draft. But if the same agent also has a tool that can reach the network (web fetch, HTTP, another mail tool), a hostile email can tell the agent to leak data through that tool. In a session that reads mail, give the agent as few network tools as possible.
|
|
140
|
+
|
|
141
|
+
## Development
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
git clone https://github.com/bcrisp4/imap-mcp
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
cd imap-mcp && uv sync
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
uv run ruff check && uv run ruff format --check && uv run pyright && uv run mypy && uv run pytest
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
To run your checkout instead of the release, use `uv run --directory /path/to/imap-mcp imap-mcp serve` as the server command.
|
|
156
|
+
|
|
157
|
+
Tests never connect to a real mailbox. They use a TLS fake IMAP server that records every command and reproduces the iCloud quirks. Read [AGENTS.md](https://github.com/bcrisp4/imap-mcp/blob/main/AGENTS.md) before you change code, and [docs/releasing.md](https://github.com/bcrisp4/imap-mcp/blob/main/docs/releasing.md) to make a release.
|
|
158
|
+
|
|
159
|
+
## Docs
|
|
160
|
+
|
|
161
|
+
- [Design](https://github.com/bcrisp4/imap-mcp/blob/main/docs/designs/imap-mcp.md): goals, security model, sync design and decisions
|
|
162
|
+
- [Changelog](https://github.com/bcrisp4/imap-mcp/blob/main/CHANGELOG.md)
|
|
163
|
+
- API reference: run `uv run pdoc imap_mcp` in a clone
|
|
164
|
+
|
|
165
|
+
## Licence
|
|
166
|
+
|
|
167
|
+
MIT. See [LICENSE](https://github.com/bcrisp4/imap-mcp/blob/main/LICENSE).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "imap-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Local, read-only MCP server with keyword search over one IMAP mailbox"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [{ name = "Ben Crisp", email = "ben@thecrisp.io" }]
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
|
+
keywords = ["mcp", "imap", "email", "icloud", "model-context-protocol"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: End Users/Desktop",
|
|
14
|
+
"Operating System :: MacOS",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Topic :: Communications :: Email :: Post-Office :: IMAP",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"beautifulsoup4>=4.15",
|
|
21
|
+
"imapclient>=4.1",
|
|
22
|
+
"lxml>=6.1",
|
|
23
|
+
"markdown-it-py>=4.2",
|
|
24
|
+
"mcp>=2.3",
|
|
25
|
+
"pydantic>=2.14",
|
|
26
|
+
"structlog>=26.1",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/bcrisp4/imap-mcp"
|
|
31
|
+
Source = "https://github.com/bcrisp4/imap-mcp"
|
|
32
|
+
Issues = "https://github.com/bcrisp4/imap-mcp/issues"
|
|
33
|
+
Design = "https://github.com/bcrisp4/imap-mcp/blob/main/docs/designs/imap-mcp.md"
|
|
34
|
+
Changelog = "https://github.com/bcrisp4/imap-mcp/blob/main/CHANGELOG.md"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
imap-mcp = "imap_mcp.entrypoints.cli:main"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling==1.33.0"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
# The sdist holds only what a rebuild and the tests need. Agent skills, CI
|
|
44
|
+
# files and generated API docs stay out.
|
|
45
|
+
[tool.hatch.build.targets.sdist]
|
|
46
|
+
only-include = [
|
|
47
|
+
"src", "tests", "scripts", "README.md", "CHANGELOG.md", "LICENSE", "uv.lock",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"anyio>=4.15",
|
|
53
|
+
"mypy>=2.4",
|
|
54
|
+
"pdoc>=16.0",
|
|
55
|
+
"pyright>=1.1.414",
|
|
56
|
+
"pytest>=9.2",
|
|
57
|
+
"pytest-asyncio>=1.4",
|
|
58
|
+
"pytest-cov>=7.1",
|
|
59
|
+
"ruff>=0.17",
|
|
60
|
+
"trustme>=1.2",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
target-version = "py314"
|
|
65
|
+
line-length = 88
|
|
66
|
+
src = ["src", "tests"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = [
|
|
70
|
+
"E", "F", "W", "I", "UP", "B", "SIM", "TC", "RUF", "PTH",
|
|
71
|
+
"ASYNC", "S", "T20", "ARG", "FBT", "A", "C4", "DTZ", "ISC",
|
|
72
|
+
"PIE", "RSE", "RET", "TID",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.flake8-type-checking]
|
|
76
|
+
runtime-evaluated-base-classes = ["pydantic.BaseModel"]
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
79
|
+
ban-relative-imports = "all"
|
|
80
|
+
|
|
81
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
82
|
+
# imap-mcp never sends mail. See docs/designs/imap-mcp.md, Security T1.
|
|
83
|
+
"smtplib".msg = "imap-mcp must never send mail (design T1)."
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint.per-file-ignores]
|
|
86
|
+
"tests/**" = ["S101", "S105", "S106", "ARG", "FBT", "PLR2004"]
|
|
87
|
+
|
|
88
|
+
[tool.pyright]
|
|
89
|
+
include = ["src", "tests"]
|
|
90
|
+
typeCheckingMode = "strict"
|
|
91
|
+
pythonVersion = "3.14"
|
|
92
|
+
reportMissingTypeStubs = false
|
|
93
|
+
|
|
94
|
+
[tool.mypy]
|
|
95
|
+
strict = true
|
|
96
|
+
python_version = "3.14"
|
|
97
|
+
mypy_path = "src"
|
|
98
|
+
packages = ["imap_mcp"]
|
|
99
|
+
|
|
100
|
+
[[tool.mypy.overrides]]
|
|
101
|
+
module = ["imapclient.*", "trustme.*"]
|
|
102
|
+
ignore_missing_imports = true
|
|
103
|
+
|
|
104
|
+
[tool.pytest.ini_options]
|
|
105
|
+
addopts = ["--strict-markers", "-ra", "--import-mode=importlib"]
|
|
106
|
+
testpaths = ["tests"]
|
|
107
|
+
pythonpath = ["src", "tests"]
|
|
108
|
+
asyncio_mode = "auto"
|
|
109
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Print the CHANGELOG.md section of one version, for the GitHub release notes.
|
|
2
|
+
|
|
3
|
+
Usage: `python scripts/changelog_section.py 0.1.0`. It exits 1 if the section
|
|
4
|
+
is missing or empty, so a release cannot go out without notes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
_HEADING = re.compile(
|
|
14
|
+
r"^## \[(?P<version>[^\]]+)\](?: - (?P<date>\d{4}-\d{2}-\d{2}))?\s*$"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def section(text: str, version: str) -> str | None:
|
|
19
|
+
"""Return the body of the `## [version] - date` section, or None.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: The changelog text.
|
|
23
|
+
version: The version, without a leading "v".
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The section body without its heading, or None if the section is
|
|
27
|
+
missing, has no release date, or is empty.
|
|
28
|
+
"""
|
|
29
|
+
lines = text.splitlines()
|
|
30
|
+
body: list[str] = []
|
|
31
|
+
found = False
|
|
32
|
+
for line in lines:
|
|
33
|
+
match = _HEADING.match(line)
|
|
34
|
+
if match:
|
|
35
|
+
if found:
|
|
36
|
+
break
|
|
37
|
+
if match["version"] == version and match["date"]:
|
|
38
|
+
found = True
|
|
39
|
+
continue
|
|
40
|
+
if found:
|
|
41
|
+
if line.startswith("[") and "]: " in line:
|
|
42
|
+
break
|
|
43
|
+
body.append(line)
|
|
44
|
+
result = "\n".join(body).strip()
|
|
45
|
+
return result or None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> int:
|
|
49
|
+
"""Print the section for `sys.argv[1]`."""
|
|
50
|
+
version = sys.argv[1].removeprefix("v")
|
|
51
|
+
text = Path("CHANGELOG.md").read_text()
|
|
52
|
+
body = section(text, version)
|
|
53
|
+
if body is None:
|
|
54
|
+
sys.stderr.write(
|
|
55
|
+
f"CHANGELOG.md has no dated, non-empty section for {version}.\n"
|
|
56
|
+
)
|
|
57
|
+
return 1
|
|
58
|
+
sys.stdout.write(body + "\n")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
sys.exit(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Fail if an installed runtime dependency has a GPL-family licence.
|
|
2
|
+
|
|
3
|
+
imap-mcp is MIT, so a GPL dependency would change the licence of the whole
|
|
4
|
+
distribution. Run with `uv run --no-dev python scripts/check_licenses.py`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from importlib.metadata import distributions
|
|
12
|
+
|
|
13
|
+
# AGPL and LGPL also match. LGPL is allowed in principle, but no dependency
|
|
14
|
+
# needs it today, so it fails here and gets a human decision.
|
|
15
|
+
_COPYLEFT = re.compile(r"\b[AL]?GPL\b|GNU (Affero |Lesser )?General Public", re.I)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> int:
|
|
19
|
+
"""Print each offending distribution and return 1 if any exist."""
|
|
20
|
+
bad: list[str] = []
|
|
21
|
+
for dist in distributions():
|
|
22
|
+
meta = dist.metadata
|
|
23
|
+
fields = [meta.get("License-Expression") or "", meta.get("License") or ""]
|
|
24
|
+
classifiers = meta.get_all("Classifier") or []
|
|
25
|
+
fields += [c for c in classifiers if c.startswith("License")]
|
|
26
|
+
text = " ".join(fields)
|
|
27
|
+
if _COPYLEFT.search(text):
|
|
28
|
+
bad.append(f"{meta['Name']} {meta['Version']}: {text[:120]}")
|
|
29
|
+
for line in bad:
|
|
30
|
+
sys.stderr.write(line + "\n")
|
|
31
|
+
return 1 if bad else 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""imap-mcp: a local, read-only MCP server over one IMAP mailbox."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The adapters package of imap-mcp."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The system clock."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SystemClock:
|
|
10
|
+
"""Reads the real time."""
|
|
11
|
+
|
|
12
|
+
def now(self) -> datetime:
|
|
13
|
+
"""Return the current time, timezone-aware UTC."""
|
|
14
|
+
return datetime.now(UTC)
|
|
15
|
+
|
|
16
|
+
def monotonic(self) -> float:
|
|
17
|
+
"""Return a monotonic time in seconds."""
|
|
18
|
+
return time.monotonic()
|