jxa-mail-mcp 0.1.0__tar.gz → 0.2.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.1.0 → jxa_mail_mcp-0.2.0}/PKG-INFO +49 -8
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/README.md +48 -7
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/pyproject.toml +1 -1
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/src/jxa_mail_mcp/jxa/mail_core.js +68 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/src/jxa_mail_mcp/server.py +212 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/.gitignore +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/LICENSE +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/src/jxa_mail_mcp/__init__.py +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/src/jxa_mail_mcp/builders.py +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/src/jxa_mail_mcp/config.py +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/src/jxa_mail_mcp/executor.py +0 -0
- {jxa_mail_mcp-0.1.0 → jxa_mail_mcp-0.2.0}/src/jxa_mail_mcp/jxa/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jxa-mail-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Fast MCP server for Apple Mail via optimized JXA scripts
|
|
5
5
|
Project-URL: Homepage, https://github.com/imdinu/jxa-mail-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/imdinu/jxa-mail-mcp
|
|
@@ -33,15 +33,27 @@ A fast MCP (Model Context Protocol) server for Apple Mail, using optimized JXA (
|
|
|
33
33
|
- **list_accounts** - List all configured email accounts
|
|
34
34
|
- **list_mailboxes** - List mailboxes for an account
|
|
35
35
|
- **get_emails** - Fetch emails from any mailbox with pagination
|
|
36
|
+
- **get_email** - Fetch a single email with full body content
|
|
36
37
|
- **get_todays_emails** - Fetch all emails received today
|
|
37
38
|
- **get_unread_emails** - Fetch unread emails
|
|
38
39
|
- **get_flagged_emails** - Fetch flagged emails
|
|
39
40
|
- **search_emails** - Search emails by subject or sender
|
|
40
41
|
- **fuzzy_search_emails** - Typo-tolerant search using trigram + Levenshtein matching
|
|
42
|
+
- **search_email_bodies** - Fuzzy search within email body content
|
|
41
43
|
|
|
42
44
|
## Installation
|
|
43
45
|
|
|
44
|
-
###
|
|
46
|
+
### No installation required
|
|
47
|
+
|
|
48
|
+
Use `pipx run` to run directly from PyPI:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pipx run jxa-mail-mcp
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### With pipx (optional)
|
|
55
|
+
|
|
56
|
+
For faster startup, install globally:
|
|
45
57
|
|
|
46
58
|
```bash
|
|
47
59
|
pipx install jxa-mail-mcp
|
|
@@ -61,26 +73,26 @@ uv sync
|
|
|
61
73
|
|
|
62
74
|
### Add to Claude Code
|
|
63
75
|
|
|
64
|
-
|
|
76
|
+
Using `pipx run` (no installation required):
|
|
65
77
|
|
|
66
78
|
```json
|
|
67
79
|
{
|
|
68
80
|
"mcpServers": {
|
|
69
81
|
"mail": {
|
|
70
|
-
"command": "
|
|
82
|
+
"command": "pipx",
|
|
83
|
+
"args": ["run", "jxa-mail-mcp"]
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
86
|
}
|
|
74
87
|
```
|
|
75
88
|
|
|
76
|
-
Or
|
|
89
|
+
Or if installed with `pipx install jxa-mail-mcp`:
|
|
77
90
|
|
|
78
91
|
```json
|
|
79
92
|
{
|
|
80
93
|
"mcpServers": {
|
|
81
94
|
"mail": {
|
|
82
|
-
"command": "
|
|
83
|
-
"args": ["run", "--directory", "/path/to/jxa-mail-mcp", "jxa-mail-mcp"]
|
|
95
|
+
"command": "jxa-mail-mcp"
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
}
|
|
@@ -89,6 +101,8 @@ Or from source:
|
|
|
89
101
|
### Run directly
|
|
90
102
|
|
|
91
103
|
```bash
|
|
104
|
+
pipx run jxa-mail-mcp
|
|
105
|
+
# or after installing
|
|
92
106
|
jxa-mail-mcp
|
|
93
107
|
```
|
|
94
108
|
|
|
@@ -119,13 +133,26 @@ Or in Claude Code config:
|
|
|
119
133
|
### Test in Python
|
|
120
134
|
|
|
121
135
|
```python
|
|
122
|
-
from jxa_mail_mcp.server import
|
|
136
|
+
from jxa_mail_mcp.server import (
|
|
137
|
+
get_todays_emails,
|
|
138
|
+
search_emails,
|
|
139
|
+
fuzzy_search_emails,
|
|
140
|
+
search_email_bodies,
|
|
141
|
+
get_email,
|
|
142
|
+
)
|
|
123
143
|
|
|
124
144
|
emails = get_todays_emails(account="iCloud", mailbox="Inbox")
|
|
125
145
|
results = search_emails("meeting", account="Work", limit=10)
|
|
126
146
|
|
|
127
147
|
# Fuzzy search - tolerates typos
|
|
128
148
|
results = fuzzy_search_emails("meetting nottes", limit=10) # finds "meeting notes"
|
|
149
|
+
|
|
150
|
+
# Search within email bodies (slower but searches full content)
|
|
151
|
+
results = search_email_bodies("project deadline", account="Work", limit=10)
|
|
152
|
+
|
|
153
|
+
# Get full email content
|
|
154
|
+
email = get_email(message_id=12345, account="Work", mailbox="INBOX")
|
|
155
|
+
print(email["content"]) # Full body text
|
|
129
156
|
```
|
|
130
157
|
|
|
131
158
|
## Architecture
|
|
@@ -203,6 +230,20 @@ The trigram pre-filtering keeps fuzzy search fast by avoiding expensive Levensht
|
|
|
203
230
|
|
|
204
231
|
**Example**: Searching for "reserch studies" (typo) correctly finds "research studies" with 0.94 similarity score.
|
|
205
232
|
|
|
233
|
+
### Body Search Performance
|
|
234
|
+
|
|
235
|
+
Body search (`search_email_bodies`) fetches full email content, which is slower than metadata-only search but enables searching within email text:
|
|
236
|
+
|
|
237
|
+
| Search Type | Time (20 emails) | Notes |
|
|
238
|
+
|-------------|------------------|-------|
|
|
239
|
+
| Metadata search | ~0.1s | Subject/sender only |
|
|
240
|
+
| Body search | ~7s | Full content fetch + fuzzy match |
|
|
241
|
+
| Single email fetch | ~0.3s | `get_email()` with full body |
|
|
242
|
+
|
|
243
|
+
Body search uses a tiered matching approach:
|
|
244
|
+
1. **Exact substring** (score 0.95) - Fast, catches most searches
|
|
245
|
+
2. **Trigram similarity** (score 0.25-0.72) - Tolerates typos without expensive Levenshtein on long text
|
|
246
|
+
|
|
206
247
|
## Development
|
|
207
248
|
|
|
208
249
|
```bash
|
|
@@ -7,15 +7,27 @@ A fast MCP (Model Context Protocol) server for Apple Mail, using optimized JXA (
|
|
|
7
7
|
- **list_accounts** - List all configured email accounts
|
|
8
8
|
- **list_mailboxes** - List mailboxes for an account
|
|
9
9
|
- **get_emails** - Fetch emails from any mailbox with pagination
|
|
10
|
+
- **get_email** - Fetch a single email with full body content
|
|
10
11
|
- **get_todays_emails** - Fetch all emails received today
|
|
11
12
|
- **get_unread_emails** - Fetch unread emails
|
|
12
13
|
- **get_flagged_emails** - Fetch flagged emails
|
|
13
14
|
- **search_emails** - Search emails by subject or sender
|
|
14
15
|
- **fuzzy_search_emails** - Typo-tolerant search using trigram + Levenshtein matching
|
|
16
|
+
- **search_email_bodies** - Fuzzy search within email body content
|
|
15
17
|
|
|
16
18
|
## Installation
|
|
17
19
|
|
|
18
|
-
###
|
|
20
|
+
### No installation required
|
|
21
|
+
|
|
22
|
+
Use `pipx run` to run directly from PyPI:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pipx run jxa-mail-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### With pipx (optional)
|
|
29
|
+
|
|
30
|
+
For faster startup, install globally:
|
|
19
31
|
|
|
20
32
|
```bash
|
|
21
33
|
pipx install jxa-mail-mcp
|
|
@@ -35,26 +47,26 @@ uv sync
|
|
|
35
47
|
|
|
36
48
|
### Add to Claude Code
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
Using `pipx run` (no installation required):
|
|
39
51
|
|
|
40
52
|
```json
|
|
41
53
|
{
|
|
42
54
|
"mcpServers": {
|
|
43
55
|
"mail": {
|
|
44
|
-
"command": "
|
|
56
|
+
"command": "pipx",
|
|
57
|
+
"args": ["run", "jxa-mail-mcp"]
|
|
45
58
|
}
|
|
46
59
|
}
|
|
47
60
|
}
|
|
48
61
|
```
|
|
49
62
|
|
|
50
|
-
Or
|
|
63
|
+
Or if installed with `pipx install jxa-mail-mcp`:
|
|
51
64
|
|
|
52
65
|
```json
|
|
53
66
|
{
|
|
54
67
|
"mcpServers": {
|
|
55
68
|
"mail": {
|
|
56
|
-
"command": "
|
|
57
|
-
"args": ["run", "--directory", "/path/to/jxa-mail-mcp", "jxa-mail-mcp"]
|
|
69
|
+
"command": "jxa-mail-mcp"
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
72
|
}
|
|
@@ -63,6 +75,8 @@ Or from source:
|
|
|
63
75
|
### Run directly
|
|
64
76
|
|
|
65
77
|
```bash
|
|
78
|
+
pipx run jxa-mail-mcp
|
|
79
|
+
# or after installing
|
|
66
80
|
jxa-mail-mcp
|
|
67
81
|
```
|
|
68
82
|
|
|
@@ -93,13 +107,26 @@ Or in Claude Code config:
|
|
|
93
107
|
### Test in Python
|
|
94
108
|
|
|
95
109
|
```python
|
|
96
|
-
from jxa_mail_mcp.server import
|
|
110
|
+
from jxa_mail_mcp.server import (
|
|
111
|
+
get_todays_emails,
|
|
112
|
+
search_emails,
|
|
113
|
+
fuzzy_search_emails,
|
|
114
|
+
search_email_bodies,
|
|
115
|
+
get_email,
|
|
116
|
+
)
|
|
97
117
|
|
|
98
118
|
emails = get_todays_emails(account="iCloud", mailbox="Inbox")
|
|
99
119
|
results = search_emails("meeting", account="Work", limit=10)
|
|
100
120
|
|
|
101
121
|
# Fuzzy search - tolerates typos
|
|
102
122
|
results = fuzzy_search_emails("meetting nottes", limit=10) # finds "meeting notes"
|
|
123
|
+
|
|
124
|
+
# Search within email bodies (slower but searches full content)
|
|
125
|
+
results = search_email_bodies("project deadline", account="Work", limit=10)
|
|
126
|
+
|
|
127
|
+
# Get full email content
|
|
128
|
+
email = get_email(message_id=12345, account="Work", mailbox="INBOX")
|
|
129
|
+
print(email["content"]) # Full body text
|
|
103
130
|
```
|
|
104
131
|
|
|
105
132
|
## Architecture
|
|
@@ -177,6 +204,20 @@ The trigram pre-filtering keeps fuzzy search fast by avoiding expensive Levensht
|
|
|
177
204
|
|
|
178
205
|
**Example**: Searching for "reserch studies" (typo) correctly finds "research studies" with 0.94 similarity score.
|
|
179
206
|
|
|
207
|
+
### Body Search Performance
|
|
208
|
+
|
|
209
|
+
Body search (`search_email_bodies`) fetches full email content, which is slower than metadata-only search but enables searching within email text:
|
|
210
|
+
|
|
211
|
+
| Search Type | Time (20 emails) | Notes |
|
|
212
|
+
|-------------|------------------|-------|
|
|
213
|
+
| Metadata search | ~0.1s | Subject/sender only |
|
|
214
|
+
| Body search | ~7s | Full content fetch + fuzzy match |
|
|
215
|
+
| Single email fetch | ~0.3s | `get_email()` with full body |
|
|
216
|
+
|
|
217
|
+
Body search uses a tiered matching approach:
|
|
218
|
+
1. **Exact substring** (score 0.95) - Fast, catches most searches
|
|
219
|
+
2. **Trigram similarity** (score 0.25-0.72) - Tolerates typos without expensive Levenshtein on long text
|
|
220
|
+
|
|
180
221
|
## Development
|
|
181
222
|
|
|
182
223
|
```bash
|
|
@@ -291,4 +291,72 @@ const MailCore = {
|
|
|
291
291
|
}
|
|
292
292
|
return null;
|
|
293
293
|
},
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Fast body search - optimized for long text.
|
|
297
|
+
* Uses substring match first, then trigram-only (skips expensive Levenshtein).
|
|
298
|
+
*
|
|
299
|
+
* @param {string} query - Search query
|
|
300
|
+
* @param {string} body - Email body content
|
|
301
|
+
* @param {number} maxChars - Max characters to search (default 2000)
|
|
302
|
+
* @returns {object|null} {score, matched, tier} or null if no match
|
|
303
|
+
*/
|
|
304
|
+
fuzzyMatchBody(query, body, maxChars = 2000) {
|
|
305
|
+
const q = (query || "").toLowerCase().trim();
|
|
306
|
+
const b = (body || "").toLowerCase().substring(0, maxChars);
|
|
307
|
+
|
|
308
|
+
if (!q || !b) return null;
|
|
309
|
+
|
|
310
|
+
// Tier 1: Exact substring match (fastest, best result)
|
|
311
|
+
const exactIndex = b.indexOf(q);
|
|
312
|
+
if (exactIndex !== -1) {
|
|
313
|
+
// Extract context around the match
|
|
314
|
+
const start = Math.max(0, exactIndex - 20);
|
|
315
|
+
const end = Math.min(b.length, exactIndex + q.length + 20);
|
|
316
|
+
const context = b.substring(start, end).trim();
|
|
317
|
+
return { score: 0.95, matched: context, tier: "exact" };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Tier 2: Word-level trigram matching (no Levenshtein)
|
|
321
|
+
// Find words that share enough trigrams with query
|
|
322
|
+
const queryTrigrams = this.trigrams(q);
|
|
323
|
+
const words = b.split(/\s+/);
|
|
324
|
+
let bestSim = 0;
|
|
325
|
+
let bestWord = null;
|
|
326
|
+
|
|
327
|
+
for (const word of words) {
|
|
328
|
+
if (word.length < 2) continue;
|
|
329
|
+
const wordTrigrams = this.trigrams(word);
|
|
330
|
+
const sim = this.trigramSimilarity(queryTrigrams, wordTrigrams);
|
|
331
|
+
if (sim > bestSim) {
|
|
332
|
+
bestSim = sim;
|
|
333
|
+
bestWord = word;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Also check multi-word phrases for multi-word queries
|
|
338
|
+
const queryWords = q.split(/\s+/).length;
|
|
339
|
+
if (queryWords > 1 && words.length >= queryWords) {
|
|
340
|
+
for (let i = 0; i <= words.length - queryWords; i++) {
|
|
341
|
+
const phrase = words.slice(i, i + queryWords).join(" ");
|
|
342
|
+
const phraseTrigrams = this.trigrams(phrase);
|
|
343
|
+
const sim = this.trigramSimilarity(queryTrigrams, phraseTrigrams);
|
|
344
|
+
if (sim > bestSim) {
|
|
345
|
+
bestSim = sim;
|
|
346
|
+
bestWord = phrase;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Require higher threshold for trigram-only matching
|
|
352
|
+
if (bestSim >= 0.25) {
|
|
353
|
+
return {
|
|
354
|
+
score: Math.round(bestSim * 0.85 * 100) / 100,
|
|
355
|
+
matched: bestWord,
|
|
356
|
+
tier: "trigram",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return null;
|
|
361
|
+
},
|
|
294
362
|
};
|
|
@@ -349,5 +349,217 @@ JSON.stringify(results.slice(0, {limit}));
|
|
|
349
349
|
return execute_with_core(script)
|
|
350
350
|
|
|
351
351
|
|
|
352
|
+
@mcp.tool
|
|
353
|
+
def search_email_bodies(
|
|
354
|
+
query: str,
|
|
355
|
+
account: str | None = None,
|
|
356
|
+
mailbox: str | None = None,
|
|
357
|
+
limit: int = 20,
|
|
358
|
+
threshold: float = 0.3,
|
|
359
|
+
) -> list[dict]:
|
|
360
|
+
"""
|
|
361
|
+
Search within email body content using fuzzy matching.
|
|
362
|
+
|
|
363
|
+
Searches the full text content of emails, not just metadata.
|
|
364
|
+
Uses a fast two-tier approach: exact substring matching first,
|
|
365
|
+
then trigram similarity for typo tolerance.
|
|
366
|
+
|
|
367
|
+
Note: Slower than metadata-only search due to fetching email bodies.
|
|
368
|
+
Use search_emails() or fuzzy_search_emails() for faster metadata search.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
query: Search term to find in email bodies
|
|
372
|
+
account: Account name. Uses JXA_MAIL_DEFAULT_ACCOUNT env var or
|
|
373
|
+
first account if not specified.
|
|
374
|
+
mailbox: Mailbox name. Uses JXA_MAIL_DEFAULT_MAILBOX env var or
|
|
375
|
+
"Inbox" if not specified.
|
|
376
|
+
limit: Maximum number of results (default: 20)
|
|
377
|
+
threshold: Minimum similarity score 0-1 (default: 0.3)
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
List of matching emails with scores, sorted by relevance.
|
|
381
|
+
Includes 'matched_in' field indicating where match was found
|
|
382
|
+
(subject, sender, or body) and 'matched_text' showing the match.
|
|
383
|
+
|
|
384
|
+
Example:
|
|
385
|
+
>>> search_email_bodies("project deadline")
|
|
386
|
+
[{"subject": "Re: Updates", "score": 0.95, "matched_in": "body",
|
|
387
|
+
"matched_text": "...project deadline is...", ...}]
|
|
388
|
+
"""
|
|
389
|
+
safe_query = query.replace("\\", "\\\\").replace("'", "\\'")
|
|
390
|
+
resolved_account = _resolve_account(account)
|
|
391
|
+
resolved_mailbox = _resolve_mailbox(mailbox)
|
|
392
|
+
|
|
393
|
+
# Build account reference
|
|
394
|
+
if resolved_account:
|
|
395
|
+
safe_account = resolved_account.replace("'", "\\'")
|
|
396
|
+
account_js = f"MailCore.getAccount('{safe_account}')"
|
|
397
|
+
else:
|
|
398
|
+
account_js = "MailCore.getAccount(null)"
|
|
399
|
+
|
|
400
|
+
safe_mailbox = resolved_mailbox.replace("'", "\\'")
|
|
401
|
+
|
|
402
|
+
script = f"""
|
|
403
|
+
const account = {account_js};
|
|
404
|
+
const mailbox = MailCore.getMailbox(account, '{safe_mailbox}');
|
|
405
|
+
const msgs = mailbox.messages;
|
|
406
|
+
const query = '{safe_query}';
|
|
407
|
+
const threshold = {threshold};
|
|
408
|
+
|
|
409
|
+
// Batch fetch properties INCLUDING content
|
|
410
|
+
const data = MailCore.batchFetch(msgs, [
|
|
411
|
+
'id', 'subject', 'sender', 'content',
|
|
412
|
+
'dateReceived', 'readStatus', 'flaggedStatus'
|
|
413
|
+
]);
|
|
414
|
+
|
|
415
|
+
const results = [];
|
|
416
|
+
const count = data.id.length;
|
|
417
|
+
|
|
418
|
+
for (let i = 0; i < count; i++) {{
|
|
419
|
+
const subject = data.subject[i] || '';
|
|
420
|
+
const sender = data.sender[i] || '';
|
|
421
|
+
const content = data.content[i] || '';
|
|
422
|
+
|
|
423
|
+
// Try matching against subject, sender, and body
|
|
424
|
+
const subjectMatch = MailCore.fuzzyMatch(query, subject);
|
|
425
|
+
const senderMatch = MailCore.fuzzyMatch(query, sender);
|
|
426
|
+
const bodyMatch = MailCore.fuzzyMatchBody(query, content);
|
|
427
|
+
|
|
428
|
+
// Take the best match across all fields
|
|
429
|
+
let bestScore = 0;
|
|
430
|
+
let matchedIn = null;
|
|
431
|
+
let matchedText = null;
|
|
432
|
+
|
|
433
|
+
if (subjectMatch && subjectMatch.score > bestScore) {{
|
|
434
|
+
bestScore = subjectMatch.score;
|
|
435
|
+
matchedIn = 'subject';
|
|
436
|
+
matchedText = subjectMatch.matched;
|
|
437
|
+
}}
|
|
438
|
+
if (senderMatch && senderMatch.score > bestScore) {{
|
|
439
|
+
bestScore = senderMatch.score;
|
|
440
|
+
matchedIn = 'sender';
|
|
441
|
+
matchedText = senderMatch.matched;
|
|
442
|
+
}}
|
|
443
|
+
if (bodyMatch && bodyMatch.score > bestScore) {{
|
|
444
|
+
bestScore = bodyMatch.score;
|
|
445
|
+
matchedIn = 'body';
|
|
446
|
+
matchedText = bodyMatch.matched;
|
|
447
|
+
}}
|
|
448
|
+
|
|
449
|
+
if (bestScore >= threshold) {{
|
|
450
|
+
results.push({{
|
|
451
|
+
id: data.id[i],
|
|
452
|
+
subject: subject,
|
|
453
|
+
sender: sender,
|
|
454
|
+
date_received: MailCore.formatDate(data.dateReceived[i]),
|
|
455
|
+
read: data.readStatus[i],
|
|
456
|
+
flagged: data.flaggedStatus[i],
|
|
457
|
+
score: Math.round(bestScore * 100) / 100,
|
|
458
|
+
matched_in: matchedIn,
|
|
459
|
+
matched_text: matchedText
|
|
460
|
+
}});
|
|
461
|
+
}}
|
|
462
|
+
}}
|
|
463
|
+
|
|
464
|
+
// Sort by score descending, then by date
|
|
465
|
+
results.sort((a, b) => {{
|
|
466
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
467
|
+
return new Date(b.date_received) - new Date(a.date_received);
|
|
468
|
+
}});
|
|
469
|
+
|
|
470
|
+
JSON.stringify(results.slice(0, {limit}));
|
|
471
|
+
"""
|
|
472
|
+
return execute_with_core(script)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@mcp.tool
|
|
476
|
+
def get_email(
|
|
477
|
+
message_id: int,
|
|
478
|
+
account: str | None = None,
|
|
479
|
+
mailbox: str | None = None,
|
|
480
|
+
) -> dict:
|
|
481
|
+
"""
|
|
482
|
+
Get a single email with full content.
|
|
483
|
+
|
|
484
|
+
Retrieves complete email details including the full body text.
|
|
485
|
+
Use this after finding an email via search to read its content.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
message_id: The email's unique ID (from search results)
|
|
489
|
+
account: Account name (optional, helps find message faster)
|
|
490
|
+
mailbox: Mailbox name (optional, helps find message faster)
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Email dictionary with full content including:
|
|
494
|
+
- id, subject, sender, date_received, date_sent
|
|
495
|
+
- content: Full plain text body
|
|
496
|
+
- read, flagged status
|
|
497
|
+
- reply_to, message_id (email Message-ID header)
|
|
498
|
+
|
|
499
|
+
Example:
|
|
500
|
+
>>> get_email(12345)
|
|
501
|
+
{"id": 12345, "subject": "Meeting notes",
|
|
502
|
+
"content": "Hi team,\\n\\nHere are the notes...", ...}
|
|
503
|
+
"""
|
|
504
|
+
resolved_account = _resolve_account(account)
|
|
505
|
+
resolved_mailbox = _resolve_mailbox(mailbox)
|
|
506
|
+
|
|
507
|
+
# Build account reference
|
|
508
|
+
if resolved_account:
|
|
509
|
+
safe_account = resolved_account.replace("'", "\\'")
|
|
510
|
+
account_js = f"MailCore.getAccount('{safe_account}')"
|
|
511
|
+
else:
|
|
512
|
+
account_js = "MailCore.getAccount(null)"
|
|
513
|
+
|
|
514
|
+
safe_mailbox = resolved_mailbox.replace("'", "\\'")
|
|
515
|
+
|
|
516
|
+
script = f"""
|
|
517
|
+
const targetId = {message_id};
|
|
518
|
+
|
|
519
|
+
// Try to find the message in the specified mailbox first
|
|
520
|
+
let msg = null;
|
|
521
|
+
const account = {account_js};
|
|
522
|
+
const mailbox = MailCore.getMailbox(account, '{safe_mailbox}');
|
|
523
|
+
|
|
524
|
+
// Search in specified mailbox
|
|
525
|
+
const ids = mailbox.messages.id();
|
|
526
|
+
const idx = ids.indexOf(targetId);
|
|
527
|
+
if (idx !== -1) {{
|
|
528
|
+
msg = mailbox.messages[idx];
|
|
529
|
+
}}
|
|
530
|
+
|
|
531
|
+
// If not found, search all mailboxes in the account
|
|
532
|
+
if (!msg) {{
|
|
533
|
+
const allMailboxes = account.mailboxes();
|
|
534
|
+
for (let i = 0; i < allMailboxes.length && !msg; i++) {{
|
|
535
|
+
const mb = allMailboxes[i];
|
|
536
|
+
const mbIds = mb.messages.id();
|
|
537
|
+
const mbIdx = mbIds.indexOf(targetId);
|
|
538
|
+
if (mbIdx !== -1) {{
|
|
539
|
+
msg = mb.messages[mbIdx];
|
|
540
|
+
}}
|
|
541
|
+
}}
|
|
542
|
+
}}
|
|
543
|
+
|
|
544
|
+
if (!msg) {{
|
|
545
|
+
throw new Error('Message not found with ID: ' + targetId);
|
|
546
|
+
}}
|
|
547
|
+
|
|
548
|
+
JSON.stringify({{
|
|
549
|
+
id: msg.id(),
|
|
550
|
+
subject: msg.subject(),
|
|
551
|
+
sender: msg.sender(),
|
|
552
|
+
content: msg.content(),
|
|
553
|
+
date_received: MailCore.formatDate(msg.dateReceived()),
|
|
554
|
+
date_sent: MailCore.formatDate(msg.dateSent()),
|
|
555
|
+
read: msg.readStatus(),
|
|
556
|
+
flagged: msg.flaggedStatus(),
|
|
557
|
+
reply_to: msg.replyTo(),
|
|
558
|
+
message_id: msg.messageId()
|
|
559
|
+
}});
|
|
560
|
+
"""
|
|
561
|
+
return execute_with_core(script)
|
|
562
|
+
|
|
563
|
+
|
|
352
564
|
if __name__ == "__main__":
|
|
353
565
|
mcp.run()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|