apple-mail-mcp 1.6.9 → 1.7.0
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.
- package/README.md +38 -0
- package/build/index.js +32 -10
- package/build/services/appleMailManager.d.ts +35 -4
- package/build/services/appleMailManager.d.ts.map +1 -1
- package/build/services/appleMailManager.js +87 -18
- package/build/services/imapClient.d.ts +75 -0
- package/build/services/imapClient.d.ts.map +1 -0
- package/build/services/imapClient.js +177 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -270,6 +270,43 @@ Then send:
|
|
|
270
270
|
|
|
271
271
|
The default `applescript` transport is unchanged; SMTP is opt-in per call.
|
|
272
272
|
|
|
273
|
+
##### IMAP backend (read/search) — opt-in, Phase 1
|
|
274
|
+
|
|
275
|
+
AppleScript runs `search`/`list` predicates client-side over the Apple Event
|
|
276
|
+
bridge, which is slow and can time out (false-empty) on large Gmail/IMAP
|
|
277
|
+
mailboxes (see [#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)). When
|
|
278
|
+
an account is configured for IMAP, `search-messages` and `list-messages` instead
|
|
279
|
+
run a **server-side IMAP search** ([#43](https://github.com/sweetrb/apple-mail-mcp/issues/43)) —
|
|
280
|
+
typically sub-second and correct on the same mailbox where AppleScript times out.
|
|
281
|
+
This is **opt-in and additive**: any account without IMAP configured behaves
|
|
282
|
+
exactly as before (AppleScript). Read-only for now — `get-message` and all
|
|
283
|
+
mutations stay on AppleScript (the IMAP rows report message **UIDs**, noted in
|
|
284
|
+
the output).
|
|
285
|
+
|
|
286
|
+
Routing is conservative: only a call whose explicit `account` matches the
|
|
287
|
+
configured IMAP account goes to IMAP; everything else falls through to
|
|
288
|
+
AppleScript.
|
|
289
|
+
|
|
290
|
+
| Variable | Required | Default | Description |
|
|
291
|
+
|----------|----------|---------|-------------|
|
|
292
|
+
| `APPLE_MAIL_MCP_IMAP_USER` | Yes | — | Login address; setting it enables IMAP |
|
|
293
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNT` | No | = user | Mail account name to match for routing |
|
|
294
|
+
| `APPLE_MAIL_MCP_IMAP_HOST` | No | `imap.gmail.com` | IMAP server hostname |
|
|
295
|
+
| `APPLE_MAIL_MCP_IMAP_PORT` | No | `993` | IMAP port (993 = implicit TLS) |
|
|
296
|
+
| `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
|
|
297
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name |
|
|
298
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
|
|
299
|
+
|
|
300
|
+
As with SMTP, the password is read from the macOS **Keychain** by default (use
|
|
301
|
+
an app-specific password for Gmail/Workspace/iCloud), so no secret goes in
|
|
302
|
+
config. Gmail label semantics: common names (`All Mail`, `Sent`, `Trash`,
|
|
303
|
+
`Spam`, `Important`, …) map to their `[Gmail]/…` IMAP paths automatically.
|
|
304
|
+
|
|
305
|
+
> Note: each call currently opens its own IMAP connection (no pooling yet), so
|
|
306
|
+
> expect a few seconds of connection overhead per call. Phase 2 (IMAP-backed
|
|
307
|
+
> mutations + folder ops, which would also resolve the IMAP slice of
|
|
308
|
+
> [#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) is not yet implemented.
|
|
309
|
+
|
|
273
310
|
---
|
|
274
311
|
|
|
275
312
|
#### `send-serial-email`
|
|
@@ -800,6 +837,7 @@ The entrypoint is written as:
|
|
|
800
837
|
| Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
|
|
801
838
|
| No smart mailboxes | Cannot access Smart Mailboxes via AppleScript |
|
|
802
839
|
| Very large mailboxes not searchable | Apple Mail's AppleScript bridge times out on mailboxes with tens of thousands of messages, so unscoped `search-messages` skips mailboxes above `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default 5000) and reports them as a partial result. Scope with `mailbox` + a date window to search inside one. ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)) |
|
|
840
|
+
| Can't delete/rename server-side mailboxes or mutate drafts | Mail.app's AppleScript bridge can only `delete`/`rename` **local "On My Mac"** mailboxes and cannot delete/move drafts — it throws `AppleEvent handler failed` for IMAP/Gmail/Workspace/iCloud/Exchange mailboxes and drafts (the GUI can do it). `delete-mailbox`/`rename-mailbox`/`delete-message`/`move-message` now return a clear "do it in Mail.app directly" error in that case instead of a generic failure. ([#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) |
|
|
803
841
|
| In-memory templates | Email templates are not persisted across server restarts |
|
|
804
842
|
| Numeric-only message IDs | Message IDs must contain only digits (validated by schema) |
|
|
805
843
|
| Batch size cap | Batch operations are limited to 100 messages per request |
|
package/build/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { AppleMailManager } from "./services/appleMailManager.js";
|
|
27
27
|
import { sendViaSmtp } from "./services/smtpMailer.js";
|
|
28
|
+
import { isImapAccount, imapSearchMessages, imapListMessages } from "./services/imapClient.js";
|
|
28
29
|
import { createSerialGate } from "./utils/serialize.js";
|
|
29
30
|
// =============================================================================
|
|
30
31
|
// Shared Validation Schemas
|
|
@@ -157,7 +158,23 @@ server.tool("search-messages", {
|
|
|
157
158
|
dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
|
|
158
159
|
dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
|
|
159
160
|
limit: z.number().optional().describe("Maximum number of results (default: 50)"),
|
|
160
|
-
}, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
|
|
161
|
+
}, withErrorHandling(async ({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
|
|
162
|
+
// IMAP backend (issue #43): server-side search when this account is
|
|
163
|
+
// explicitly configured for IMAP; otherwise fall through to AppleScript.
|
|
164
|
+
if (isImapAccount(account)) {
|
|
165
|
+
return successResponse(await imapSearchMessages({
|
|
166
|
+
query,
|
|
167
|
+
mailbox,
|
|
168
|
+
account,
|
|
169
|
+
limit,
|
|
170
|
+
dateFrom,
|
|
171
|
+
dateTo,
|
|
172
|
+
from,
|
|
173
|
+
subject,
|
|
174
|
+
isRead,
|
|
175
|
+
isFlagged,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
161
178
|
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged);
|
|
162
179
|
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
163
180
|
if (messages.length === 0) {
|
|
@@ -200,7 +217,12 @@ server.tool("list-messages", {
|
|
|
200
217
|
offset: z.number().optional().describe("Number of messages to skip (for pagination)"),
|
|
201
218
|
from: z.string().optional().describe("Filter by sender email address or name"),
|
|
202
219
|
unreadOnly: z.boolean().optional().describe("Only show unread messages"),
|
|
203
|
-
}, withErrorHandling(({ mailbox, account, limit = 50, offset = 0, from }) => {
|
|
220
|
+
}, withErrorHandling(async ({ mailbox, account, limit = 50, offset = 0, from, unreadOnly }) => {
|
|
221
|
+
// IMAP backend (issue #43): server-side listing when this account is
|
|
222
|
+
// explicitly configured for IMAP; otherwise fall through to AppleScript.
|
|
223
|
+
if (isImapAccount(account)) {
|
|
224
|
+
return successResponse(await imapListMessages({ mailbox, account, limit, offset, from, unreadOnly }));
|
|
225
|
+
}
|
|
204
226
|
const { messages, diagnostics } = mailManager.listMessagesWithDiagnostics(mailbox, account, limit, from, offset);
|
|
205
227
|
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
206
228
|
if (messages.length === 0) {
|
|
@@ -383,9 +405,9 @@ server.tool("unflag-message", {
|
|
|
383
405
|
server.tool("delete-message", {
|
|
384
406
|
id: MESSAGE_ID_SCHEMA,
|
|
385
407
|
}, withErrorHandling(({ id }) => {
|
|
386
|
-
const success = mailManager.deleteMessage(id);
|
|
408
|
+
const { success, error } = mailManager.deleteMessage(id);
|
|
387
409
|
if (!success) {
|
|
388
|
-
return errorResponse(`Failed to delete message "${id}"`);
|
|
410
|
+
return errorResponse(error || `Failed to delete message "${id}"`);
|
|
389
411
|
}
|
|
390
412
|
return successResponse("Message deleted");
|
|
391
413
|
}, "Error deleting message"));
|
|
@@ -395,9 +417,9 @@ server.tool("move-message", {
|
|
|
395
417
|
mailbox: z.string().min(1, "Destination mailbox is required"),
|
|
396
418
|
account: z.string().optional().describe("Account containing the destination mailbox"),
|
|
397
419
|
}, withErrorHandling(({ id, mailbox, account }) => {
|
|
398
|
-
const success = mailManager.moveMessage(id, mailbox, account);
|
|
420
|
+
const { success, error } = mailManager.moveMessage(id, mailbox, account);
|
|
399
421
|
if (!success) {
|
|
400
|
-
return errorResponse(`Failed to move message to "${mailbox}"`);
|
|
422
|
+
return errorResponse(error || `Failed to move message to "${mailbox}"`);
|
|
401
423
|
}
|
|
402
424
|
return successResponse(`Message moved to "${mailbox}"`);
|
|
403
425
|
}, "Error moving message"));
|
|
@@ -572,9 +594,9 @@ server.tool("delete-mailbox", {
|
|
|
572
594
|
name: z.string().min(1, "Mailbox name is required"),
|
|
573
595
|
account: z.string().optional().describe("Account containing the mailbox"),
|
|
574
596
|
}, withErrorHandling(({ name, account }) => {
|
|
575
|
-
const success = mailManager.deleteMailbox(name, account);
|
|
597
|
+
const { success, error } = mailManager.deleteMailbox(name, account);
|
|
576
598
|
if (!success) {
|
|
577
|
-
return errorResponse(`Failed to delete mailbox "${name}"`);
|
|
599
|
+
return errorResponse(error || `Failed to delete mailbox "${name}"`);
|
|
578
600
|
}
|
|
579
601
|
return successResponse(`Mailbox "${name}" deleted`);
|
|
580
602
|
}, "Error deleting mailbox"));
|
|
@@ -584,9 +606,9 @@ server.tool("rename-mailbox", {
|
|
|
584
606
|
newName: z.string().min(1, "New mailbox name is required"),
|
|
585
607
|
account: z.string().optional().describe("Account containing the mailbox"),
|
|
586
608
|
}, withErrorHandling(({ oldName, newName, account }) => {
|
|
587
|
-
const success = mailManager.renameMailbox(oldName, newName, account);
|
|
609
|
+
const { success, error } = mailManager.renameMailbox(oldName, newName, account);
|
|
588
610
|
if (!success) {
|
|
589
|
-
return errorResponse(`Failed to rename mailbox "${oldName}" to "${newName}"`);
|
|
611
|
+
return errorResponse(error || `Failed to rename mailbox "${oldName}" to "${newName}"`);
|
|
590
612
|
}
|
|
591
613
|
return successResponse(`Mailbox renamed from "${oldName}" to "${newName}"`);
|
|
592
614
|
}, "Error renaming mailbox"));
|
|
@@ -46,6 +46,14 @@ export declare function splitSearchDiagnostics(output: string, account: string):
|
|
|
46
46
|
* (caller passes `resolve(...)` output).
|
|
47
47
|
*/
|
|
48
48
|
export declare function isPathWithinAllowedRoots(resolvedPath: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Turn a raw mailbox delete/rename failure into an actionable, non-retryable
|
|
51
|
+
* message when it's the known server-side-mailbox limitation (#42); otherwise
|
|
52
|
+
* return the raw error unchanged.
|
|
53
|
+
*
|
|
54
|
+
* Exported for unit testing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function describeMailboxOpError(op: "delete" | "rename", raw: string): string;
|
|
49
57
|
export declare function escapeForAppleScript(text: string): string;
|
|
50
58
|
/**
|
|
51
59
|
* Emits AppleScript that builds a date into the variable `varName` from numeric
|
|
@@ -328,7 +336,21 @@ export declare class AppleMailManager {
|
|
|
328
336
|
/**
|
|
329
337
|
* Delete a message.
|
|
330
338
|
*/
|
|
331
|
-
deleteMessage(id: string):
|
|
339
|
+
deleteMessage(id: string): {
|
|
340
|
+
success: boolean;
|
|
341
|
+
error?: string;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Classify a failed message mutation (delete/move) into an actionable error.
|
|
345
|
+
*
|
|
346
|
+
* Mail.app's scripting bridge cannot delete or move drafts, and cannot mutate
|
|
347
|
+
* messages in some server-side special mailboxes — it throws `AppleEvent
|
|
348
|
+
* handler failed` rather than a useful message (#42). When that pattern is
|
|
349
|
+
* seen, look up the message's mailbox (cheap, indexed `whose id is`) to give a
|
|
350
|
+
* draft-specific or server-specific hint. Other errors (e.g. "Message not
|
|
351
|
+
* found", "ambiguous destination") pass through unchanged.
|
|
352
|
+
*/
|
|
353
|
+
private classifyMessageMutationError;
|
|
332
354
|
/**
|
|
333
355
|
* Move a message to a different mailbox.
|
|
334
356
|
*/
|
|
@@ -351,7 +373,10 @@ export declare class AppleMailManager {
|
|
|
351
373
|
* (destination not found / ambiguous / message not found).
|
|
352
374
|
*/
|
|
353
375
|
private moveMessageInternal;
|
|
354
|
-
moveMessage(id: string, mailbox: string, account?: string):
|
|
376
|
+
moveMessage(id: string, mailbox: string, account?: string): {
|
|
377
|
+
success: boolean;
|
|
378
|
+
error?: string;
|
|
379
|
+
};
|
|
355
380
|
/**
|
|
356
381
|
* Run one operation over many message IDs in a SINGLE osascript invocation.
|
|
357
382
|
*
|
|
@@ -424,11 +449,17 @@ export declare class AppleMailManager {
|
|
|
424
449
|
/**
|
|
425
450
|
* Delete a mailbox.
|
|
426
451
|
*/
|
|
427
|
-
deleteMailbox(name: string, account?: string):
|
|
452
|
+
deleteMailbox(name: string, account?: string): {
|
|
453
|
+
success: boolean;
|
|
454
|
+
error?: string;
|
|
455
|
+
};
|
|
428
456
|
/**
|
|
429
457
|
* Rename a mailbox by creating a new one, moving messages, and deleting the old one.
|
|
430
458
|
*/
|
|
431
|
-
renameMailbox(oldName: string, newName: string, account?: string):
|
|
459
|
+
renameMailbox(oldName: string, newName: string, account?: string): {
|
|
460
|
+
success: boolean;
|
|
461
|
+
error?: string;
|
|
462
|
+
};
|
|
432
463
|
/**
|
|
433
464
|
* List all mail accounts (uses cache).
|
|
434
465
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -158,6 +158,29 @@ export function isPathWithinAllowedRoots(resolvedPath) {
|
|
|
158
158
|
return resolvedPath === base || resolvedPath.startsWith(base + sep);
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Pattern in a raw AppleScript error that indicates an operation Mail.app's
|
|
163
|
+
* scripting interface simply cannot perform on this target — most often a
|
|
164
|
+
* server-side (IMAP / Gmail / Workspace / iCloud / Exchange) mailbox or a draft.
|
|
165
|
+
* Mail throws `AppleEvent handler failed` (-10000) for these; the GUI can do
|
|
166
|
+
* them, the scripting bridge cannot. See issue #42 and the audit doc.
|
|
167
|
+
*/
|
|
168
|
+
const UNSUPPORTED_APPLESCRIPT_OP = /AppleEvent handler failed|-10000/i;
|
|
169
|
+
/**
|
|
170
|
+
* Turn a raw mailbox delete/rename failure into an actionable, non-retryable
|
|
171
|
+
* message when it's the known server-side-mailbox limitation (#42); otherwise
|
|
172
|
+
* return the raw error unchanged.
|
|
173
|
+
*
|
|
174
|
+
* Exported for unit testing.
|
|
175
|
+
*/
|
|
176
|
+
export function describeMailboxOpError(op, raw) {
|
|
177
|
+
const trimmed = (raw || "").trim();
|
|
178
|
+
if (UNSUPPORTED_APPLESCRIPT_OP.test(trimmed)) {
|
|
179
|
+
const verb = op === "delete" ? "Delete" : "Rename";
|
|
180
|
+
return `Mail.app cannot ${op} server-side (IMAP / Gmail / Workspace / iCloud / Exchange) mailboxes via AppleScript — only local "On My Mac" mailboxes support this. ${verb} it in Mail.app directly. (Mail.app error: ${trimmed})`;
|
|
181
|
+
}
|
|
182
|
+
return trimmed || `Failed to ${op} mailbox`;
|
|
183
|
+
}
|
|
161
184
|
export function escapeForAppleScript(text) {
|
|
162
185
|
if (!text)
|
|
163
186
|
return "";
|
|
@@ -1425,11 +1448,41 @@ export class AppleMailManager {
|
|
|
1425
1448
|
deleteMessage(id) {
|
|
1426
1449
|
const script = this.findMessageScript(id, "delete msg");
|
|
1427
1450
|
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
1428
|
-
if (
|
|
1429
|
-
|
|
1430
|
-
return false;
|
|
1451
|
+
if (result.success && !result.output.startsWith("error:")) {
|
|
1452
|
+
return { success: true };
|
|
1431
1453
|
}
|
|
1432
|
-
|
|
1454
|
+
const raw = result.success
|
|
1455
|
+
? result.output.replace(/^error:/, "")
|
|
1456
|
+
: result.error || "Unknown error";
|
|
1457
|
+
const error = this.classifyMessageMutationError(id, raw, "delete");
|
|
1458
|
+
console.error(`Failed to delete message: ${error}`);
|
|
1459
|
+
return { success: false, error };
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Classify a failed message mutation (delete/move) into an actionable error.
|
|
1463
|
+
*
|
|
1464
|
+
* Mail.app's scripting bridge cannot delete or move drafts, and cannot mutate
|
|
1465
|
+
* messages in some server-side special mailboxes — it throws `AppleEvent
|
|
1466
|
+
* handler failed` rather than a useful message (#42). When that pattern is
|
|
1467
|
+
* seen, look up the message's mailbox (cheap, indexed `whose id is`) to give a
|
|
1468
|
+
* draft-specific or server-specific hint. Other errors (e.g. "Message not
|
|
1469
|
+
* found", "ambiguous destination") pass through unchanged.
|
|
1470
|
+
*/
|
|
1471
|
+
classifyMessageMutationError(id, raw, op) {
|
|
1472
|
+
const trimmed = (raw || "").trim();
|
|
1473
|
+
if (!UNSUPPORTED_APPLESCRIPT_OP.test(trimmed))
|
|
1474
|
+
return trimmed || `Failed to ${op} message`;
|
|
1475
|
+
let mailbox = "";
|
|
1476
|
+
try {
|
|
1477
|
+
mailbox = this.getMessageById(id)?.mailbox ?? "";
|
|
1478
|
+
}
|
|
1479
|
+
catch {
|
|
1480
|
+
/* best-effort; fall through to the generic server-side message */
|
|
1481
|
+
}
|
|
1482
|
+
if (/draft/i.test(mailbox)) {
|
|
1483
|
+
return `Mail.app cannot ${op} drafts via AppleScript; ${op} it in Mail.app directly. (Mail.app error: ${trimmed})`;
|
|
1484
|
+
}
|
|
1485
|
+
return `Mail.app cannot ${op} this message via AppleScript (server-side or special mailbox${mailbox ? ` "${mailbox}"` : ""}); ${op} it in Mail.app directly. (Mail.app error: ${trimmed})`;
|
|
1433
1486
|
}
|
|
1434
1487
|
/**
|
|
1435
1488
|
* Move a message to a different mailbox.
|
|
@@ -1501,11 +1554,12 @@ export class AppleMailManager {
|
|
|
1501
1554
|
return { success: true };
|
|
1502
1555
|
}
|
|
1503
1556
|
moveMessage(id, mailbox, account) {
|
|
1504
|
-
const
|
|
1505
|
-
if (
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1557
|
+
const res = this.moveMessageInternal(id, mailbox, account);
|
|
1558
|
+
if (res.success)
|
|
1559
|
+
return { success: true };
|
|
1560
|
+
const error = this.classifyMessageMutationError(id, res.error || "Failed to move message", "move");
|
|
1561
|
+
console.error(`Failed to move message: ${error}`);
|
|
1562
|
+
return { success: false, error };
|
|
1509
1563
|
}
|
|
1510
1564
|
// ===========================================================================
|
|
1511
1565
|
// Batch Operations
|
|
@@ -1938,11 +1992,15 @@ export class AppleMailManager {
|
|
|
1938
1992
|
`);
|
|
1939
1993
|
const result = executeAppleScript(script);
|
|
1940
1994
|
if (!result.success || result.output.startsWith("error:")) {
|
|
1941
|
-
|
|
1942
|
-
|
|
1995
|
+
const raw = result.success
|
|
1996
|
+
? result.output.replace(/^error:/, "")
|
|
1997
|
+
: result.error || "Unknown error";
|
|
1998
|
+
const error = describeMailboxOpError("delete", raw);
|
|
1999
|
+
console.error(`Failed to delete mailbox: ${error}`);
|
|
2000
|
+
return { success: false, error };
|
|
1943
2001
|
}
|
|
1944
2002
|
this.invalidateCache();
|
|
1945
|
-
return true;
|
|
2003
|
+
return { success: true };
|
|
1946
2004
|
}
|
|
1947
2005
|
/**
|
|
1948
2006
|
* Rename a mailbox by creating a new one, moving messages, and deleting the old one.
|
|
@@ -1951,7 +2009,10 @@ export class AppleMailManager {
|
|
|
1951
2009
|
const targetAccount = this.resolveAccount(account);
|
|
1952
2010
|
// Create the new mailbox
|
|
1953
2011
|
if (!this.createMailbox(newName, targetAccount)) {
|
|
1954
|
-
return
|
|
2012
|
+
return {
|
|
2013
|
+
success: false,
|
|
2014
|
+
error: `Could not create the destination mailbox "${newName}" needed for the rename.`,
|
|
2015
|
+
};
|
|
1955
2016
|
}
|
|
1956
2017
|
// Move all messages from old to new
|
|
1957
2018
|
const resolvedOld = this.resolveMailbox(oldName, targetAccount);
|
|
@@ -1997,19 +2058,27 @@ export class AppleMailManager {
|
|
|
1997
2058
|
// check), so a truncated move is recoverable rather than lossy.
|
|
1998
2059
|
const result = executeAppleScript(moveScript, { timeoutMs: 120000 });
|
|
1999
2060
|
if (!result.success || result.output.startsWith("error:")) {
|
|
2000
|
-
|
|
2001
|
-
|
|
2061
|
+
const raw = result.success
|
|
2062
|
+
? result.output.replace(/^error:/, "")
|
|
2063
|
+
: result.error || "Unknown error";
|
|
2064
|
+
// The empty destination mailbox we just created is now an orphan; the
|
|
2065
|
+
// source is untouched (delete only runs after a verified-empty move).
|
|
2066
|
+
const error = describeMailboxOpError("rename", raw);
|
|
2067
|
+
console.error(`Failed to rename mailbox: ${error}`);
|
|
2068
|
+
this.invalidateCache();
|
|
2069
|
+
return { success: false, error };
|
|
2002
2070
|
}
|
|
2003
2071
|
if (result.output.startsWith("partial")) {
|
|
2004
2072
|
const parts = result.output.split(FIELD_SEP);
|
|
2005
2073
|
const remaining = parts[3] ?? "?";
|
|
2006
2074
|
const total = parts[2] ?? "?";
|
|
2007
|
-
|
|
2075
|
+
const error = `Only ${parts[1] ?? "?"} of ${total} messages moved, ${remaining} remain in "${resolvedOld}"; the source was NOT deleted (both mailboxes left intact). Retry to move the rest.`;
|
|
2076
|
+
console.error(`Failed to rename mailbox: ${error}`);
|
|
2008
2077
|
this.invalidateCache(); // the new mailbox now exists and holds the moved messages
|
|
2009
|
-
return false;
|
|
2078
|
+
return { success: false, error };
|
|
2010
2079
|
}
|
|
2011
2080
|
this.invalidateCache();
|
|
2012
|
-
return true;
|
|
2081
|
+
return { success: true };
|
|
2013
2082
|
}
|
|
2014
2083
|
// ===========================================================================
|
|
2015
2084
|
// Account Operations
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export declare const IMAP_ENV: {
|
|
2
|
+
readonly user: "APPLE_MAIL_MCP_IMAP_USER";
|
|
3
|
+
readonly account: "APPLE_MAIL_MCP_IMAP_ACCOUNT";
|
|
4
|
+
readonly host: "APPLE_MAIL_MCP_IMAP_HOST";
|
|
5
|
+
readonly port: "APPLE_MAIL_MCP_IMAP_PORT";
|
|
6
|
+
readonly password: "APPLE_MAIL_MCP_IMAP_PASSWORD";
|
|
7
|
+
readonly keychainService: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE";
|
|
8
|
+
readonly keychainAccount: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT";
|
|
9
|
+
};
|
|
10
|
+
export interface ImapConfig {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
secure: boolean;
|
|
14
|
+
user: string;
|
|
15
|
+
pass: string;
|
|
16
|
+
accountLabel: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ImapSearchArgs {
|
|
19
|
+
query?: string;
|
|
20
|
+
account?: string;
|
|
21
|
+
from?: string;
|
|
22
|
+
subject?: string;
|
|
23
|
+
mailbox?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
dateFrom?: string;
|
|
26
|
+
dateTo?: string;
|
|
27
|
+
isRead?: boolean;
|
|
28
|
+
isFlagged?: boolean;
|
|
29
|
+
unreadOnly?: boolean;
|
|
30
|
+
offset?: number;
|
|
31
|
+
}
|
|
32
|
+
interface ImapAddress {
|
|
33
|
+
name?: string;
|
|
34
|
+
address?: string;
|
|
35
|
+
}
|
|
36
|
+
interface ImapEnvelope {
|
|
37
|
+
subject?: string;
|
|
38
|
+
date?: Date | string;
|
|
39
|
+
from?: ImapAddress[];
|
|
40
|
+
}
|
|
41
|
+
interface ImapMessage {
|
|
42
|
+
uid: number;
|
|
43
|
+
envelope?: ImapEnvelope;
|
|
44
|
+
flags?: Set<string>;
|
|
45
|
+
}
|
|
46
|
+
interface MailboxLock {
|
|
47
|
+
release: () => void;
|
|
48
|
+
}
|
|
49
|
+
export interface ImapClientLike {
|
|
50
|
+
connect(): Promise<void>;
|
|
51
|
+
getMailboxLock(path: string): Promise<MailboxLock>;
|
|
52
|
+
search(query: Record<string, unknown>, opts: {
|
|
53
|
+
uid: true;
|
|
54
|
+
}): Promise<number[] | false>;
|
|
55
|
+
fetch(range: string, query: Record<string, unknown>, opts: {
|
|
56
|
+
uid: true;
|
|
57
|
+
}): AsyncIterable<ImapMessage>;
|
|
58
|
+
logout(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export type ImapConnect = (cfg: ImapConfig) => Promise<ImapClientLike>;
|
|
61
|
+
/** True only when IMAP is configured AND the explicit `account` matches it. */
|
|
62
|
+
export declare function isImapAccount(account: string | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
63
|
+
export declare function resolveImapConfig(env?: NodeJS.ProcessEnv): ImapConfig;
|
|
64
|
+
/** Map common (Gmail) mailbox names to their IMAP paths. */
|
|
65
|
+
export declare function resolveMailboxPath(mailbox: string | undefined, mode: "search" | "list"): string;
|
|
66
|
+
export declare function imapSearchMessages(args: ImapSearchArgs, deps?: {
|
|
67
|
+
connect?: ImapConnect;
|
|
68
|
+
config?: ImapConfig;
|
|
69
|
+
}): Promise<string>;
|
|
70
|
+
export declare function imapListMessages(args: ImapSearchArgs, deps?: {
|
|
71
|
+
connect?: ImapConnect;
|
|
72
|
+
config?: ImapConfig;
|
|
73
|
+
}): Promise<string>;
|
|
74
|
+
export {};
|
|
75
|
+
//# sourceMappingURL=imapClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,QAAQ;;;;;;;;CAQX,CAAC;AAEX,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,WAAW;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,WAAW,EAAE,CAAC;CACtB;AACD,UAAU,WAAW;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACrB;AACD,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AACD,MAAM,WAAW,cAAc;IAC7B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACnD,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE;QAAE,GAAG,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC;IACvF,KAAK,CACH,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,IAAI,EAAE;QAAE,GAAG,EAAE,IAAI,CAAA;KAAE,GAClB,aAAa,CAAC,WAAW,CAAC,CAAC;IAC9B,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;AAEvE,+EAA+E;AAC/E,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAKT;AAED,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,UAAU,CA6BlF;AAcD,4DAA4D;AAC5D,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAc/F;AA8ED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAO,GACxD,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAO,GACxD,OAAO,CAAC,MAAM,CAAC,CAEjB"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAP backend for read/search (issue #43, Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* AppleScript-over-Mail.app is the default. When an account is explicitly
|
|
5
|
+
* configured for IMAP (env below), `search-messages` and `list-messages` route
|
|
6
|
+
* here instead, running a SERVER-SIDE search — orders of magnitude faster than
|
|
7
|
+
* AppleScript's client-side `whose` enumeration on large Gmail mailboxes, and
|
|
8
|
+
* correct (no false-empty on timeout). Read-only; mutations stay on AppleScript.
|
|
9
|
+
*
|
|
10
|
+
* Opt-in via env (mirrors the SMTP transport pattern):
|
|
11
|
+
* APPLE_MAIL_MCP_IMAP_USER (required — enables IMAP; the login address)
|
|
12
|
+
* APPLE_MAIL_MCP_IMAP_ACCOUNT (Mail account name to match for routing; default = USER)
|
|
13
|
+
* APPLE_MAIL_MCP_IMAP_HOST (default imap.gmail.com)
|
|
14
|
+
* APPLE_MAIL_MCP_IMAP_PORT (default 993, implicit TLS)
|
|
15
|
+
* APPLE_MAIL_MCP_IMAP_PASSWORD (else Keychain via the two vars below)
|
|
16
|
+
* APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE / _KEYCHAIN_ACCOUNT
|
|
17
|
+
*
|
|
18
|
+
* @module services/imapClient
|
|
19
|
+
*/
|
|
20
|
+
import { ImapFlow } from "imapflow";
|
|
21
|
+
import { readKeychainPassword } from "../services/smtpMailer.js";
|
|
22
|
+
export const IMAP_ENV = {
|
|
23
|
+
user: "APPLE_MAIL_MCP_IMAP_USER",
|
|
24
|
+
account: "APPLE_MAIL_MCP_IMAP_ACCOUNT",
|
|
25
|
+
host: "APPLE_MAIL_MCP_IMAP_HOST",
|
|
26
|
+
port: "APPLE_MAIL_MCP_IMAP_PORT",
|
|
27
|
+
password: "APPLE_MAIL_MCP_IMAP_PASSWORD",
|
|
28
|
+
keychainService: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE",
|
|
29
|
+
keychainAccount: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT",
|
|
30
|
+
};
|
|
31
|
+
/** True only when IMAP is configured AND the explicit `account` matches it. */
|
|
32
|
+
export function isImapAccount(account, env = process.env) {
|
|
33
|
+
const user = env[IMAP_ENV.user]?.trim();
|
|
34
|
+
if (!user || !account)
|
|
35
|
+
return false;
|
|
36
|
+
const label = env[IMAP_ENV.account]?.trim() || user;
|
|
37
|
+
return account === label || account === user;
|
|
38
|
+
}
|
|
39
|
+
export function resolveImapConfig(env = process.env) {
|
|
40
|
+
const user = env[IMAP_ENV.user]?.trim();
|
|
41
|
+
if (!user) {
|
|
42
|
+
throw new Error(`IMAP not configured. Set ${IMAP_ENV.user} (login address) to enable it.`);
|
|
43
|
+
}
|
|
44
|
+
const host = env[IMAP_ENV.host]?.trim() || "imap.gmail.com";
|
|
45
|
+
const port = env[IMAP_ENV.port] ? Number.parseInt(env[IMAP_ENV.port], 10) : 993;
|
|
46
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
47
|
+
throw new Error(`Invalid ${IMAP_ENV.port}: "${env[IMAP_ENV.port]}".`);
|
|
48
|
+
}
|
|
49
|
+
let pass = env[IMAP_ENV.password];
|
|
50
|
+
if (!pass) {
|
|
51
|
+
const svc = env[IMAP_ENV.keychainService]?.trim();
|
|
52
|
+
const acct = env[IMAP_ENV.keychainAccount]?.trim() || user;
|
|
53
|
+
if (svc)
|
|
54
|
+
pass = readKeychainPassword(svc, acct) ?? undefined;
|
|
55
|
+
}
|
|
56
|
+
if (!pass) {
|
|
57
|
+
throw new Error(`No IMAP password. Set ${IMAP_ENV.password}, or ${IMAP_ENV.keychainService}/${IMAP_ENV.keychainAccount} for the Keychain.`);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
host,
|
|
61
|
+
port,
|
|
62
|
+
secure: port === 993,
|
|
63
|
+
user,
|
|
64
|
+
pass,
|
|
65
|
+
accountLabel: env[IMAP_ENV.account]?.trim() || user,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const defaultConnect = async (cfg) => {
|
|
69
|
+
const client = new ImapFlow({
|
|
70
|
+
host: cfg.host,
|
|
71
|
+
port: cfg.port,
|
|
72
|
+
secure: cfg.secure,
|
|
73
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
74
|
+
logger: false,
|
|
75
|
+
});
|
|
76
|
+
await client.connect();
|
|
77
|
+
return client;
|
|
78
|
+
};
|
|
79
|
+
/** Map common (Gmail) mailbox names to their IMAP paths. */
|
|
80
|
+
export function resolveMailboxPath(mailbox, mode) {
|
|
81
|
+
if (!mailbox)
|
|
82
|
+
return mode === "search" ? "[Gmail]/All Mail" : "INBOX";
|
|
83
|
+
const map = {
|
|
84
|
+
"all mail": "[Gmail]/All Mail",
|
|
85
|
+
"sent mail": "[Gmail]/Sent Mail",
|
|
86
|
+
sent: "[Gmail]/Sent Mail",
|
|
87
|
+
trash: "[Gmail]/Trash",
|
|
88
|
+
drafts: "[Gmail]/Drafts",
|
|
89
|
+
spam: "[Gmail]/Spam",
|
|
90
|
+
junk: "[Gmail]/Spam",
|
|
91
|
+
starred: "[Gmail]/Starred",
|
|
92
|
+
important: "[Gmail]/Important",
|
|
93
|
+
};
|
|
94
|
+
return map[mailbox.trim().toLowerCase()] ?? mailbox;
|
|
95
|
+
}
|
|
96
|
+
function buildCriteria(a, listMode) {
|
|
97
|
+
const c = {};
|
|
98
|
+
if (a.query)
|
|
99
|
+
c.or = [{ subject: a.query }, { from: a.query }];
|
|
100
|
+
if (a.from)
|
|
101
|
+
c.from = a.from;
|
|
102
|
+
if (a.subject)
|
|
103
|
+
c.subject = a.subject;
|
|
104
|
+
if (a.isRead === true)
|
|
105
|
+
c.seen = true;
|
|
106
|
+
if (a.isRead === false)
|
|
107
|
+
c.unseen = true;
|
|
108
|
+
if (a.unreadOnly && listMode)
|
|
109
|
+
c.unseen = true;
|
|
110
|
+
if (a.isFlagged === true)
|
|
111
|
+
c.flagged = true;
|
|
112
|
+
if (a.isFlagged === false)
|
|
113
|
+
c.unflagged = true;
|
|
114
|
+
if (a.dateFrom)
|
|
115
|
+
c.since = new Date(a.dateFrom);
|
|
116
|
+
if (a.dateTo)
|
|
117
|
+
c.before = new Date(a.dateTo);
|
|
118
|
+
if (Object.keys(c).length === 0)
|
|
119
|
+
c.all = true;
|
|
120
|
+
return c;
|
|
121
|
+
}
|
|
122
|
+
function formatRow(m) {
|
|
123
|
+
const env = m.envelope ?? {};
|
|
124
|
+
const subject = env.subject || "(no subject)";
|
|
125
|
+
const a = env.from?.[0];
|
|
126
|
+
const from = a
|
|
127
|
+
? a.name
|
|
128
|
+
? `${a.name} <${a.address ?? ""}>`
|
|
129
|
+
: (a.address ?? "(unknown)")
|
|
130
|
+
: "(unknown)";
|
|
131
|
+
const date = env.date ? new Date(env.date).toLocaleDateString() : "";
|
|
132
|
+
const read = m.flags?.has("\\Seen") ? "read" : "unread";
|
|
133
|
+
return ` - UID: ${m.uid} | ${date} | ${subject} (from: ${from}) [${read}]`;
|
|
134
|
+
}
|
|
135
|
+
async function run(args, listMode, deps) {
|
|
136
|
+
const cfg = deps.config ?? resolveImapConfig();
|
|
137
|
+
const client = await (deps.connect ?? defaultConnect)(cfg);
|
|
138
|
+
try {
|
|
139
|
+
const path = resolveMailboxPath(args.mailbox, listMode ? "list" : "search");
|
|
140
|
+
const lock = await client.getMailboxLock(path);
|
|
141
|
+
try {
|
|
142
|
+
const found = await client.search(buildCriteria(args, listMode), { uid: true });
|
|
143
|
+
const uids = Array.isArray(found) ? found : [];
|
|
144
|
+
if (uids.length === 0) {
|
|
145
|
+
return `No messages found via IMAP in "${path}" (account ${cfg.accountLabel}).`;
|
|
146
|
+
}
|
|
147
|
+
const limit = args.limit ?? 50;
|
|
148
|
+
const offset = args.offset ?? 0;
|
|
149
|
+
// UIDs are ascending → newest are the highest. Apply offset+limit from the newest end.
|
|
150
|
+
const newest = uids
|
|
151
|
+
.slice()
|
|
152
|
+
.reverse()
|
|
153
|
+
.slice(offset, offset + limit);
|
|
154
|
+
const byUid = new Map();
|
|
155
|
+
for await (const msg of client.fetch(newest.join(","), { envelope: true, flags: true }, { uid: true })) {
|
|
156
|
+
byUid.set(msg.uid, formatRow(msg));
|
|
157
|
+
}
|
|
158
|
+
const rows = newest.map((u) => byUid.get(u)).filter((r) => Boolean(r));
|
|
159
|
+
const verb = listMode ? "listed" : "matched";
|
|
160
|
+
return (`Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, mailbox "${path}"; ${uids.length} total ${verb}):\n` +
|
|
161
|
+
rows.join("\n") +
|
|
162
|
+
`\n\nNote: IDs are IMAP UIDs. get-message and message mutations still use the AppleScript path (Phase 1 is read/search only).`);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
lock.release();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await client.logout().catch(() => undefined);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function imapSearchMessages(args, deps = {}) {
|
|
173
|
+
return run(args, false, deps);
|
|
174
|
+
}
|
|
175
|
+
export function imapListMessages(args, deps = {}) {
|
|
176
|
+
return run(args, true, deps);
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-mail-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
62
|
+
"imapflow": "^1.4.1",
|
|
62
63
|
"nodemailer": "^9.0.0",
|
|
63
64
|
"zod": "^3.22.4"
|
|
64
65
|
},
|