jxa-mail-mcp 0.1.0__tar.gz → 0.3.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.
- jxa_mail_mcp-0.3.0/PKG-INFO +355 -0
- jxa_mail_mcp-0.3.0/README.md +328 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/pyproject.toml +11 -2
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/__init__.py +18 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/cli.py +358 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/config.py +81 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/executor.py +258 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/__init__.py +14 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/disk.py +485 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/manager.py +458 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/schema.py +277 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/search.py +331 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/sync.py +305 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/watcher.py +341 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/src/jxa_mail_mcp/jxa/mail_core.js +68 -0
- jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/server.py +814 -0
- jxa_mail_mcp-0.1.0/PKG-INFO +0 -223
- jxa_mail_mcp-0.1.0/README.md +0 -197
- jxa_mail_mcp-0.1.0/src/jxa_mail_mcp/__init__.py +0 -10
- jxa_mail_mcp-0.1.0/src/jxa_mail_mcp/config.py +0 -29
- jxa_mail_mcp-0.1.0/src/jxa_mail_mcp/executor.py +0 -84
- jxa_mail_mcp-0.1.0/src/jxa_mail_mcp/server.py +0 -353
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/.gitignore +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/LICENSE +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/src/jxa_mail_mcp/builders.py +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/src/jxa_mail_mcp/jxa/__init__.py +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jxa-mail-mcp
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Fast MCP server for Apple Mail with FTS5 search index
|
|
5
|
+
Project-URL: Homepage, https://github.com/imdinu/jxa-mail-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/imdinu/jxa-mail-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/imdinu/jxa-mail-mcp/issues
|
|
8
|
+
Author-email: Ioan-Mihail Dinu <iodinu@icloud.com>
|
|
9
|
+
License-Expression: GPL-3.0-or-later
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: apple-mail,automation,email,jxa,macos,mcp,model-context-protocol
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: MacOS X
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Programming Language :: JavaScript
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Communications :: Email
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
24
|
+
Requires-Dist: cyclopts>=5.0.0a1
|
|
25
|
+
Requires-Dist: fastmcp<4,>=3.0.0b1
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# JXA Mail MCP
|
|
29
|
+
|
|
30
|
+
[](https://www.python.org/downloads/)
|
|
31
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
32
|
+
[](https://www.apple.com/macos/)
|
|
33
|
+
[](https://modelcontextprotocol.io/)
|
|
34
|
+
[](https://github.com/astral-sh/ruff)
|
|
35
|
+
|
|
36
|
+
A fast MCP (Model Context Protocol) server for Apple Mail, using optimized JXA (JavaScript for Automation) scripts with batch property fetching for **87x faster** performance, plus an optional **FTS5 search index** for **700-3500x faster** body search (~2ms vs ~7s).
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
### Email Tools
|
|
41
|
+
- **list_accounts** - List all configured email accounts
|
|
42
|
+
- **list_mailboxes** - List mailboxes for an account
|
|
43
|
+
- **get_emails** - Fetch emails from any mailbox with pagination
|
|
44
|
+
- **get_email** - Fetch a single email with full body content
|
|
45
|
+
- **get_todays_emails** - Fetch all emails received today
|
|
46
|
+
- **get_unread_emails** - Fetch unread emails
|
|
47
|
+
- **get_flagged_emails** - Fetch flagged emails
|
|
48
|
+
- **search_emails** - Search emails by subject or sender
|
|
49
|
+
- **fuzzy_search_emails** - Typo-tolerant search using trigram + Levenshtein matching
|
|
50
|
+
- **search_email_bodies** - Full-text search within email bodies (~100x faster with index)
|
|
51
|
+
|
|
52
|
+
### Index Tools
|
|
53
|
+
- **index_status** - Get FTS5 index statistics
|
|
54
|
+
- **sync_index** - Sync new emails to the index
|
|
55
|
+
- **rebuild_index** - Force rebuild the index from disk
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
### No installation required
|
|
60
|
+
|
|
61
|
+
Use `pipx run` to run directly from PyPI:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pipx run jxa-mail-mcp
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### With pipx (optional)
|
|
68
|
+
|
|
69
|
+
For faster startup, install globally:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pipx install jxa-mail-mcp
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### From source
|
|
76
|
+
|
|
77
|
+
Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/imdinu/jxa-mail-mcp
|
|
81
|
+
cd jxa-mail-mcp
|
|
82
|
+
uv sync
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
### 1. Add to Claude Code
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"mail": {
|
|
93
|
+
"command": "jxa-mail-mcp"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2. Build the Search Index (Optional but Recommended)
|
|
100
|
+
|
|
101
|
+
For instant body search (~2ms instead of ~7s), build the FTS5 index:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Grant Full Disk Access to Terminal first:
|
|
105
|
+
# System Settings → Privacy & Security → Full Disk Access → Add Terminal
|
|
106
|
+
|
|
107
|
+
jxa-mail-mcp index --verbose
|
|
108
|
+
# → Indexed 22,696 emails in 1m 7.6s
|
|
109
|
+
# → Database size: 130.5 MB
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Use with Claude
|
|
113
|
+
|
|
114
|
+
Once configured, you can search emails, get today's messages, find unread emails, and more through natural conversation.
|
|
115
|
+
|
|
116
|
+
## CLI Commands
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
jxa-mail-mcp # Run MCP server (default)
|
|
120
|
+
jxa-mail-mcp serve # Run MCP server explicitly
|
|
121
|
+
jxa-mail-mcp --watch # Run with real-time index updates
|
|
122
|
+
jxa-mail-mcp index # Build search index from disk
|
|
123
|
+
jxa-mail-mcp status # Show index statistics
|
|
124
|
+
jxa-mail-mcp rebuild # Force rebuild index
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Real-Time Index Updates
|
|
128
|
+
|
|
129
|
+
Use `--watch` to automatically update the index when new emails arrive:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
jxa-mail-mcp --watch
|
|
133
|
+
# or
|
|
134
|
+
jxa-mail-mcp serve --watch
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The file watcher monitors `~/Library/Mail/V10/` for `.emlx` changes and updates the index in real-time. Requires Full Disk Access.
|
|
138
|
+
|
|
139
|
+
## Configuration
|
|
140
|
+
|
|
141
|
+
### Environment Variables
|
|
142
|
+
|
|
143
|
+
| Variable | Default | Description |
|
|
144
|
+
|----------|---------|-------------|
|
|
145
|
+
| `JXA_MAIL_DEFAULT_ACCOUNT` | First account | Default email account |
|
|
146
|
+
| `JXA_MAIL_DEFAULT_MAILBOX` | `Inbox` | Default mailbox |
|
|
147
|
+
| `JXA_MAIL_INDEX_PATH` | `~/.jxa-mail-mcp/index.db` | Index database location |
|
|
148
|
+
| `JXA_MAIL_INDEX_MAX_EMAILS` | `5000` | Max emails per mailbox to index |
|
|
149
|
+
| `JXA_MAIL_INDEX_STALENESS_HOURS` | `24` | Hours before index is stale |
|
|
150
|
+
|
|
151
|
+
### Claude Code Config
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"mcpServers": {
|
|
156
|
+
"mail": {
|
|
157
|
+
"command": "jxa-mail-mcp",
|
|
158
|
+
"env": {
|
|
159
|
+
"JXA_MAIL_DEFAULT_ACCOUNT": "Work"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## FTS5 Search Index
|
|
167
|
+
|
|
168
|
+
The FTS5 index makes `search_email_bodies()` ~100x faster by pre-indexing email content.
|
|
169
|
+
|
|
170
|
+
### How It Works
|
|
171
|
+
|
|
172
|
+
1. **Build from disk**: `jxa-mail-mcp index` reads `.emlx` files directly (~30x faster than JXA)
|
|
173
|
+
2. **Startup sync**: New emails are synced via JXA when the server starts
|
|
174
|
+
3. **Real-time updates**: `--watch` flag enables file watcher for automatic index updates
|
|
175
|
+
4. **Fast search**: Queries use SQLite FTS5 with BM25 ranking
|
|
176
|
+
|
|
177
|
+
### Requirements
|
|
178
|
+
|
|
179
|
+
Building the index requires **Full Disk Access** for Terminal:
|
|
180
|
+
1. Open **System Settings**
|
|
181
|
+
2. Go to **Privacy & Security → Full Disk Access**
|
|
182
|
+
3. Add and enable **Terminal.app** (or your terminal emulator)
|
|
183
|
+
4. Restart terminal
|
|
184
|
+
|
|
185
|
+
The MCP server itself does NOT need Full Disk Access (uses JXA for syncing).
|
|
186
|
+
|
|
187
|
+
### Performance Comparison
|
|
188
|
+
|
|
189
|
+
| Operation | Without Index | With Index | Speedup |
|
|
190
|
+
|-----------|---------------|------------|---------|
|
|
191
|
+
| Body search | ~7,000ms | ~2-10ms | **700-3500x** |
|
|
192
|
+
| Initial index build | N/A | ~1-2 min | One-time |
|
|
193
|
+
| Startup sync | N/A | ~1-2s | Incremental |
|
|
194
|
+
| Index size | N/A | ~6 KB/email | - |
|
|
195
|
+
|
|
196
|
+
#### Real-World Benchmarks (22,696 emails)
|
|
197
|
+
|
|
198
|
+
| Query | Results | Time |
|
|
199
|
+
|-------|---------|------|
|
|
200
|
+
| "invoice" | 20 | 2.5ms |
|
|
201
|
+
| "meeting tomorrow" | 20 | 1.3ms |
|
|
202
|
+
| "password reset" | 20 | 0.6ms |
|
|
203
|
+
| "shipping confirmation" | 10 | 4.1ms |
|
|
204
|
+
|
|
205
|
+
## Architecture
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
src/jxa_mail_mcp/
|
|
209
|
+
├── __init__.py # CLI entry point
|
|
210
|
+
├── cli.py # CLI commands (index, status, rebuild)
|
|
211
|
+
├── server.py # FastMCP server and MCP tools
|
|
212
|
+
├── config.py # Environment variable configuration
|
|
213
|
+
├── builders.py # QueryBuilder for constructing JXA scripts
|
|
214
|
+
├── executor.py # Async JXA script execution utilities
|
|
215
|
+
├── index/ # FTS5 search index module
|
|
216
|
+
│ ├── __init__.py # Exports IndexManager
|
|
217
|
+
│ ├── schema.py # SQLite schema, migrations, and utilities
|
|
218
|
+
│ ├── manager.py # IndexManager class
|
|
219
|
+
│ ├── disk.py # Direct .emlx file reading
|
|
220
|
+
│ ├── sync.py # JXA-based incremental sync
|
|
221
|
+
│ ├── search.py # FTS5 search functions
|
|
222
|
+
│ └── watcher.py # Real-time file watcher
|
|
223
|
+
└── jxa/
|
|
224
|
+
├── __init__.py # Exports MAIL_CORE_JS
|
|
225
|
+
└── mail_core.js # Shared JXA utilities library
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Design Principles
|
|
229
|
+
|
|
230
|
+
1. **Separation of concerns**: Python handles logic/types, JavaScript handles Mail.app interaction
|
|
231
|
+
2. **Builder pattern**: `QueryBuilder` constructs optimized JXA scripts programmatically
|
|
232
|
+
3. **Shared JS library**: `mail_core.js` provides reusable utilities injected into all scripts
|
|
233
|
+
4. **Hybrid indexing**: Disk reading for speed, JXA for incremental updates
|
|
234
|
+
5. **Async execution**: All JXA calls use `asyncio.create_subprocess_exec` for non-blocking I/O
|
|
235
|
+
6. **Type safety**: Python type hints and TypedDict for clear API contracts
|
|
236
|
+
|
|
237
|
+
### Hybrid Access Pattern
|
|
238
|
+
|
|
239
|
+
| Access Method | Use Case | Latency | When Used |
|
|
240
|
+
|---------------|----------|---------|-----------|
|
|
241
|
+
| **JXA (Live)** | Real-time ops, small queries | ~100-300ms | `get_email()`, `list_mailboxes()` |
|
|
242
|
+
| **FTS5 (Cached)** | Body search, complex filtering | ~2-10ms | `search_email_bodies()` |
|
|
243
|
+
| **Disk (Batch)** | Initial indexing | ~15ms/100 emails | `jxa-mail-mcp index` |
|
|
244
|
+
|
|
245
|
+
## Performance
|
|
246
|
+
|
|
247
|
+
### Batch Property Fetching (87x faster)
|
|
248
|
+
|
|
249
|
+
Naive AppleScript/JXA iteration is extremely slow because each property access triggers a separate Apple Event IPC round-trip. We use batch property fetching instead:
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
// FAST: ~0.6 seconds (87x faster than per-message iteration)
|
|
253
|
+
const msgs = inbox.messages;
|
|
254
|
+
const senders = msgs.sender(); // Single IPC call returns array
|
|
255
|
+
const subjects = msgs.subject(); // Single IPC call returns array
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Benchmark Results
|
|
259
|
+
|
|
260
|
+
| Method | Time | Speedup |
|
|
261
|
+
|--------|------|---------|
|
|
262
|
+
| AppleScript (per-message) | 54.1s | 1x |
|
|
263
|
+
| JXA (per-message) | 53.9s | 1x |
|
|
264
|
+
| **JXA (batch fetching)** | **0.62s** | **87x** |
|
|
265
|
+
|
|
266
|
+
### Body Search with FTS5 Index
|
|
267
|
+
|
|
268
|
+
| Search Type | Without Index | With Index | Speedup |
|
|
269
|
+
|-------------|---------------|------------|---------|
|
|
270
|
+
| Body search | ~7,000ms | ~2-10ms | **700-3500x** |
|
|
271
|
+
| Metadata search | ~100ms | ~100ms | - |
|
|
272
|
+
|
|
273
|
+
The FTS5 index uses:
|
|
274
|
+
- **Porter stemmer**: "meeting" matches "meetings", "met"
|
|
275
|
+
- **BM25 ranking**: Results sorted by relevance (term frequency × inverse document frequency)
|
|
276
|
+
- **External content table**: Shares storage with main emails table for efficiency
|
|
277
|
+
|
|
278
|
+
### Fuzzy Search Performance
|
|
279
|
+
|
|
280
|
+
Fuzzy search uses trigrams for fast candidate selection and Levenshtein distance for accurate ranking:
|
|
281
|
+
|
|
282
|
+
| Search Type | Time (~6,000 emails) |
|
|
283
|
+
|-------------|---------------------|
|
|
284
|
+
| Regular search | ~360ms |
|
|
285
|
+
| Fuzzy search | ~480ms (+33%) |
|
|
286
|
+
|
|
287
|
+
## Development
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
uv sync
|
|
291
|
+
uv run ruff check src/
|
|
292
|
+
uv run ruff format src/
|
|
293
|
+
|
|
294
|
+
# Run unit tests
|
|
295
|
+
uv run pytest
|
|
296
|
+
|
|
297
|
+
# Run tests with verbose output
|
|
298
|
+
uv run pytest -v
|
|
299
|
+
|
|
300
|
+
# Manual test
|
|
301
|
+
uv run python -c "
|
|
302
|
+
import asyncio
|
|
303
|
+
from jxa_mail_mcp.server import list_accounts, get_todays_emails
|
|
304
|
+
print('Accounts:', len(asyncio.run(list_accounts())))
|
|
305
|
+
print('Today:', len(asyncio.run(get_todays_emails())))
|
|
306
|
+
"
|
|
307
|
+
|
|
308
|
+
# Test index
|
|
309
|
+
uv run python -c "
|
|
310
|
+
from jxa_mail_mcp.index import IndexManager
|
|
311
|
+
m = IndexManager.get_instance()
|
|
312
|
+
if m.has_index():
|
|
313
|
+
stats = m.get_stats()
|
|
314
|
+
print(f'Indexed: {stats.email_count} emails')
|
|
315
|
+
"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Security
|
|
319
|
+
|
|
320
|
+
### Implemented Protections
|
|
321
|
+
|
|
322
|
+
| Threat | Mitigation | Location |
|
|
323
|
+
|--------|------------|----------|
|
|
324
|
+
| SQL Injection | Parameterized queries | `search.py`, `sync.py` |
|
|
325
|
+
| JXA Injection | `json.dumps()` serialization | `sync.py`, `executor.py` |
|
|
326
|
+
| FTS5 Query Injection | Special character escaping | `search.py` |
|
|
327
|
+
| XSS via HTML Emails | BeautifulSoup HTML parsing | `disk.py` |
|
|
328
|
+
| DoS via Large Files | 25 MB file size limit | `disk.py` |
|
|
329
|
+
| Path Traversal | Path validation in watcher | `watcher.py` |
|
|
330
|
+
| Data Exposure | Database created with 0600 permissions | `schema.py` |
|
|
331
|
+
|
|
332
|
+
## Troubleshooting
|
|
333
|
+
|
|
334
|
+
### ModuleNotFoundError after install
|
|
335
|
+
|
|
336
|
+
If you get `ModuleNotFoundError: No module named 'jxa_mail_mcp'` even though
|
|
337
|
+
the package is installed, reset the virtual environment:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
rm -rf .venv
|
|
341
|
+
uv sync --upgrade
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Full Disk Access denied
|
|
345
|
+
|
|
346
|
+
The `jxa-mail-mcp index` command requires Full Disk Access to read Mail.app's
|
|
347
|
+
data files. Grant access in:
|
|
348
|
+
|
|
349
|
+
**System Settings → Privacy & Security → Full Disk Access → Add Terminal**
|
|
350
|
+
|
|
351
|
+
Then restart your terminal.
|
|
352
|
+
|
|
353
|
+
## License
|
|
354
|
+
|
|
355
|
+
GPL-3.0-or-later
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# JXA Mail MCP
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
5
|
+
[](https://www.apple.com/macos/)
|
|
6
|
+
[](https://modelcontextprotocol.io/)
|
|
7
|
+
[](https://github.com/astral-sh/ruff)
|
|
8
|
+
|
|
9
|
+
A fast MCP (Model Context Protocol) server for Apple Mail, using optimized JXA (JavaScript for Automation) scripts with batch property fetching for **87x faster** performance, plus an optional **FTS5 search index** for **700-3500x faster** body search (~2ms vs ~7s).
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
### Email Tools
|
|
14
|
+
- **list_accounts** - List all configured email accounts
|
|
15
|
+
- **list_mailboxes** - List mailboxes for an account
|
|
16
|
+
- **get_emails** - Fetch emails from any mailbox with pagination
|
|
17
|
+
- **get_email** - Fetch a single email with full body content
|
|
18
|
+
- **get_todays_emails** - Fetch all emails received today
|
|
19
|
+
- **get_unread_emails** - Fetch unread emails
|
|
20
|
+
- **get_flagged_emails** - Fetch flagged emails
|
|
21
|
+
- **search_emails** - Search emails by subject or sender
|
|
22
|
+
- **fuzzy_search_emails** - Typo-tolerant search using trigram + Levenshtein matching
|
|
23
|
+
- **search_email_bodies** - Full-text search within email bodies (~100x faster with index)
|
|
24
|
+
|
|
25
|
+
### Index Tools
|
|
26
|
+
- **index_status** - Get FTS5 index statistics
|
|
27
|
+
- **sync_index** - Sync new emails to the index
|
|
28
|
+
- **rebuild_index** - Force rebuild the index from disk
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### No installation required
|
|
33
|
+
|
|
34
|
+
Use `pipx run` to run directly from PyPI:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pipx run jxa-mail-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### With pipx (optional)
|
|
41
|
+
|
|
42
|
+
For faster startup, install globally:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pipx install jxa-mail-mcp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### From source
|
|
49
|
+
|
|
50
|
+
Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/imdinu/jxa-mail-mcp
|
|
54
|
+
cd jxa-mail-mcp
|
|
55
|
+
uv sync
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### 1. Add to Claude Code
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"mail": {
|
|
66
|
+
"command": "jxa-mail-mcp"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Build the Search Index (Optional but Recommended)
|
|
73
|
+
|
|
74
|
+
For instant body search (~2ms instead of ~7s), build the FTS5 index:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Grant Full Disk Access to Terminal first:
|
|
78
|
+
# System Settings → Privacy & Security → Full Disk Access → Add Terminal
|
|
79
|
+
|
|
80
|
+
jxa-mail-mcp index --verbose
|
|
81
|
+
# → Indexed 22,696 emails in 1m 7.6s
|
|
82
|
+
# → Database size: 130.5 MB
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3. Use with Claude
|
|
86
|
+
|
|
87
|
+
Once configured, you can search emails, get today's messages, find unread emails, and more through natural conversation.
|
|
88
|
+
|
|
89
|
+
## CLI Commands
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
jxa-mail-mcp # Run MCP server (default)
|
|
93
|
+
jxa-mail-mcp serve # Run MCP server explicitly
|
|
94
|
+
jxa-mail-mcp --watch # Run with real-time index updates
|
|
95
|
+
jxa-mail-mcp index # Build search index from disk
|
|
96
|
+
jxa-mail-mcp status # Show index statistics
|
|
97
|
+
jxa-mail-mcp rebuild # Force rebuild index
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Real-Time Index Updates
|
|
101
|
+
|
|
102
|
+
Use `--watch` to automatically update the index when new emails arrive:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
jxa-mail-mcp --watch
|
|
106
|
+
# or
|
|
107
|
+
jxa-mail-mcp serve --watch
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The file watcher monitors `~/Library/Mail/V10/` for `.emlx` changes and updates the index in real-time. Requires Full Disk Access.
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
### Environment Variables
|
|
115
|
+
|
|
116
|
+
| Variable | Default | Description |
|
|
117
|
+
|----------|---------|-------------|
|
|
118
|
+
| `JXA_MAIL_DEFAULT_ACCOUNT` | First account | Default email account |
|
|
119
|
+
| `JXA_MAIL_DEFAULT_MAILBOX` | `Inbox` | Default mailbox |
|
|
120
|
+
| `JXA_MAIL_INDEX_PATH` | `~/.jxa-mail-mcp/index.db` | Index database location |
|
|
121
|
+
| `JXA_MAIL_INDEX_MAX_EMAILS` | `5000` | Max emails per mailbox to index |
|
|
122
|
+
| `JXA_MAIL_INDEX_STALENESS_HOURS` | `24` | Hours before index is stale |
|
|
123
|
+
|
|
124
|
+
### Claude Code Config
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"mcpServers": {
|
|
129
|
+
"mail": {
|
|
130
|
+
"command": "jxa-mail-mcp",
|
|
131
|
+
"env": {
|
|
132
|
+
"JXA_MAIL_DEFAULT_ACCOUNT": "Work"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## FTS5 Search Index
|
|
140
|
+
|
|
141
|
+
The FTS5 index makes `search_email_bodies()` ~100x faster by pre-indexing email content.
|
|
142
|
+
|
|
143
|
+
### How It Works
|
|
144
|
+
|
|
145
|
+
1. **Build from disk**: `jxa-mail-mcp index` reads `.emlx` files directly (~30x faster than JXA)
|
|
146
|
+
2. **Startup sync**: New emails are synced via JXA when the server starts
|
|
147
|
+
3. **Real-time updates**: `--watch` flag enables file watcher for automatic index updates
|
|
148
|
+
4. **Fast search**: Queries use SQLite FTS5 with BM25 ranking
|
|
149
|
+
|
|
150
|
+
### Requirements
|
|
151
|
+
|
|
152
|
+
Building the index requires **Full Disk Access** for Terminal:
|
|
153
|
+
1. Open **System Settings**
|
|
154
|
+
2. Go to **Privacy & Security → Full Disk Access**
|
|
155
|
+
3. Add and enable **Terminal.app** (or your terminal emulator)
|
|
156
|
+
4. Restart terminal
|
|
157
|
+
|
|
158
|
+
The MCP server itself does NOT need Full Disk Access (uses JXA for syncing).
|
|
159
|
+
|
|
160
|
+
### Performance Comparison
|
|
161
|
+
|
|
162
|
+
| Operation | Without Index | With Index | Speedup |
|
|
163
|
+
|-----------|---------------|------------|---------|
|
|
164
|
+
| Body search | ~7,000ms | ~2-10ms | **700-3500x** |
|
|
165
|
+
| Initial index build | N/A | ~1-2 min | One-time |
|
|
166
|
+
| Startup sync | N/A | ~1-2s | Incremental |
|
|
167
|
+
| Index size | N/A | ~6 KB/email | - |
|
|
168
|
+
|
|
169
|
+
#### Real-World Benchmarks (22,696 emails)
|
|
170
|
+
|
|
171
|
+
| Query | Results | Time |
|
|
172
|
+
|-------|---------|------|
|
|
173
|
+
| "invoice" | 20 | 2.5ms |
|
|
174
|
+
| "meeting tomorrow" | 20 | 1.3ms |
|
|
175
|
+
| "password reset" | 20 | 0.6ms |
|
|
176
|
+
| "shipping confirmation" | 10 | 4.1ms |
|
|
177
|
+
|
|
178
|
+
## Architecture
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
src/jxa_mail_mcp/
|
|
182
|
+
├── __init__.py # CLI entry point
|
|
183
|
+
├── cli.py # CLI commands (index, status, rebuild)
|
|
184
|
+
├── server.py # FastMCP server and MCP tools
|
|
185
|
+
├── config.py # Environment variable configuration
|
|
186
|
+
├── builders.py # QueryBuilder for constructing JXA scripts
|
|
187
|
+
├── executor.py # Async JXA script execution utilities
|
|
188
|
+
├── index/ # FTS5 search index module
|
|
189
|
+
│ ├── __init__.py # Exports IndexManager
|
|
190
|
+
│ ├── schema.py # SQLite schema, migrations, and utilities
|
|
191
|
+
│ ├── manager.py # IndexManager class
|
|
192
|
+
│ ├── disk.py # Direct .emlx file reading
|
|
193
|
+
│ ├── sync.py # JXA-based incremental sync
|
|
194
|
+
│ ├── search.py # FTS5 search functions
|
|
195
|
+
│ └── watcher.py # Real-time file watcher
|
|
196
|
+
└── jxa/
|
|
197
|
+
├── __init__.py # Exports MAIL_CORE_JS
|
|
198
|
+
└── mail_core.js # Shared JXA utilities library
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Design Principles
|
|
202
|
+
|
|
203
|
+
1. **Separation of concerns**: Python handles logic/types, JavaScript handles Mail.app interaction
|
|
204
|
+
2. **Builder pattern**: `QueryBuilder` constructs optimized JXA scripts programmatically
|
|
205
|
+
3. **Shared JS library**: `mail_core.js` provides reusable utilities injected into all scripts
|
|
206
|
+
4. **Hybrid indexing**: Disk reading for speed, JXA for incremental updates
|
|
207
|
+
5. **Async execution**: All JXA calls use `asyncio.create_subprocess_exec` for non-blocking I/O
|
|
208
|
+
6. **Type safety**: Python type hints and TypedDict for clear API contracts
|
|
209
|
+
|
|
210
|
+
### Hybrid Access Pattern
|
|
211
|
+
|
|
212
|
+
| Access Method | Use Case | Latency | When Used |
|
|
213
|
+
|---------------|----------|---------|-----------|
|
|
214
|
+
| **JXA (Live)** | Real-time ops, small queries | ~100-300ms | `get_email()`, `list_mailboxes()` |
|
|
215
|
+
| **FTS5 (Cached)** | Body search, complex filtering | ~2-10ms | `search_email_bodies()` |
|
|
216
|
+
| **Disk (Batch)** | Initial indexing | ~15ms/100 emails | `jxa-mail-mcp index` |
|
|
217
|
+
|
|
218
|
+
## Performance
|
|
219
|
+
|
|
220
|
+
### Batch Property Fetching (87x faster)
|
|
221
|
+
|
|
222
|
+
Naive AppleScript/JXA iteration is extremely slow because each property access triggers a separate Apple Event IPC round-trip. We use batch property fetching instead:
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
// FAST: ~0.6 seconds (87x faster than per-message iteration)
|
|
226
|
+
const msgs = inbox.messages;
|
|
227
|
+
const senders = msgs.sender(); // Single IPC call returns array
|
|
228
|
+
const subjects = msgs.subject(); // Single IPC call returns array
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Benchmark Results
|
|
232
|
+
|
|
233
|
+
| Method | Time | Speedup |
|
|
234
|
+
|--------|------|---------|
|
|
235
|
+
| AppleScript (per-message) | 54.1s | 1x |
|
|
236
|
+
| JXA (per-message) | 53.9s | 1x |
|
|
237
|
+
| **JXA (batch fetching)** | **0.62s** | **87x** |
|
|
238
|
+
|
|
239
|
+
### Body Search with FTS5 Index
|
|
240
|
+
|
|
241
|
+
| Search Type | Without Index | With Index | Speedup |
|
|
242
|
+
|-------------|---------------|------------|---------|
|
|
243
|
+
| Body search | ~7,000ms | ~2-10ms | **700-3500x** |
|
|
244
|
+
| Metadata search | ~100ms | ~100ms | - |
|
|
245
|
+
|
|
246
|
+
The FTS5 index uses:
|
|
247
|
+
- **Porter stemmer**: "meeting" matches "meetings", "met"
|
|
248
|
+
- **BM25 ranking**: Results sorted by relevance (term frequency × inverse document frequency)
|
|
249
|
+
- **External content table**: Shares storage with main emails table for efficiency
|
|
250
|
+
|
|
251
|
+
### Fuzzy Search Performance
|
|
252
|
+
|
|
253
|
+
Fuzzy search uses trigrams for fast candidate selection and Levenshtein distance for accurate ranking:
|
|
254
|
+
|
|
255
|
+
| Search Type | Time (~6,000 emails) |
|
|
256
|
+
|-------------|---------------------|
|
|
257
|
+
| Regular search | ~360ms |
|
|
258
|
+
| Fuzzy search | ~480ms (+33%) |
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
uv sync
|
|
264
|
+
uv run ruff check src/
|
|
265
|
+
uv run ruff format src/
|
|
266
|
+
|
|
267
|
+
# Run unit tests
|
|
268
|
+
uv run pytest
|
|
269
|
+
|
|
270
|
+
# Run tests with verbose output
|
|
271
|
+
uv run pytest -v
|
|
272
|
+
|
|
273
|
+
# Manual test
|
|
274
|
+
uv run python -c "
|
|
275
|
+
import asyncio
|
|
276
|
+
from jxa_mail_mcp.server import list_accounts, get_todays_emails
|
|
277
|
+
print('Accounts:', len(asyncio.run(list_accounts())))
|
|
278
|
+
print('Today:', len(asyncio.run(get_todays_emails())))
|
|
279
|
+
"
|
|
280
|
+
|
|
281
|
+
# Test index
|
|
282
|
+
uv run python -c "
|
|
283
|
+
from jxa_mail_mcp.index import IndexManager
|
|
284
|
+
m = IndexManager.get_instance()
|
|
285
|
+
if m.has_index():
|
|
286
|
+
stats = m.get_stats()
|
|
287
|
+
print(f'Indexed: {stats.email_count} emails')
|
|
288
|
+
"
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Security
|
|
292
|
+
|
|
293
|
+
### Implemented Protections
|
|
294
|
+
|
|
295
|
+
| Threat | Mitigation | Location |
|
|
296
|
+
|--------|------------|----------|
|
|
297
|
+
| SQL Injection | Parameterized queries | `search.py`, `sync.py` |
|
|
298
|
+
| JXA Injection | `json.dumps()` serialization | `sync.py`, `executor.py` |
|
|
299
|
+
| FTS5 Query Injection | Special character escaping | `search.py` |
|
|
300
|
+
| XSS via HTML Emails | BeautifulSoup HTML parsing | `disk.py` |
|
|
301
|
+
| DoS via Large Files | 25 MB file size limit | `disk.py` |
|
|
302
|
+
| Path Traversal | Path validation in watcher | `watcher.py` |
|
|
303
|
+
| Data Exposure | Database created with 0600 permissions | `schema.py` |
|
|
304
|
+
|
|
305
|
+
## Troubleshooting
|
|
306
|
+
|
|
307
|
+
### ModuleNotFoundError after install
|
|
308
|
+
|
|
309
|
+
If you get `ModuleNotFoundError: No module named 'jxa_mail_mcp'` even though
|
|
310
|
+
the package is installed, reset the virtual environment:
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
rm -rf .venv
|
|
314
|
+
uv sync --upgrade
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Full Disk Access denied
|
|
318
|
+
|
|
319
|
+
The `jxa-mail-mcp index` command requires Full Disk Access to read Mail.app's
|
|
320
|
+
data files. Grant access in:
|
|
321
|
+
|
|
322
|
+
**System Settings → Privacy & Security → Full Disk Access → Add Terminal**
|
|
323
|
+
|
|
324
|
+
Then restart your terminal.
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
GPL-3.0-or-later
|