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.
Files changed (26) hide show
  1. jxa_mail_mcp-0.3.0/PKG-INFO +355 -0
  2. jxa_mail_mcp-0.3.0/README.md +328 -0
  3. {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/pyproject.toml +11 -2
  4. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/__init__.py +18 -0
  5. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/cli.py +358 -0
  6. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/config.py +81 -0
  7. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/executor.py +258 -0
  8. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/__init__.py +14 -0
  9. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/disk.py +485 -0
  10. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/manager.py +458 -0
  11. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/schema.py +277 -0
  12. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/search.py +331 -0
  13. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/sync.py +305 -0
  14. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/index/watcher.py +341 -0
  15. {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/src/jxa_mail_mcp/jxa/mail_core.js +68 -0
  16. jxa_mail_mcp-0.3.0/src/jxa_mail_mcp/server.py +814 -0
  17. jxa_mail_mcp-0.1.0/PKG-INFO +0 -223
  18. jxa_mail_mcp-0.1.0/README.md +0 -197
  19. jxa_mail_mcp-0.1.0/src/jxa_mail_mcp/__init__.py +0 -10
  20. jxa_mail_mcp-0.1.0/src/jxa_mail_mcp/config.py +0 -29
  21. jxa_mail_mcp-0.1.0/src/jxa_mail_mcp/executor.py +0 -84
  22. jxa_mail_mcp-0.1.0/src/jxa_mail_mcp/server.py +0 -353
  23. {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/.gitignore +0 -0
  24. {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/LICENSE +0 -0
  25. {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.3.0}/src/jxa_mail_mcp/builders.py +0 -0
  26. {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
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
31
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
32
+ [![macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)](https://www.apple.com/macos/)
33
+ [![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io/)
34
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](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
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
4
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
5
+ [![macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)](https://www.apple.com/macos/)
6
+ [![MCP](https://img.shields.io/badge/MCP-compatible-green.svg)](https://modelcontextprotocol.io/)
7
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](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