apple-mail-mcp 1.6.4 → 1.6.6

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.
@@ -21,11 +21,12 @@ import type { Message, MessageContent, Mailbox, Account, Attachment, HealthCheck
21
21
  export declare function mergeSearchDiagnostics(into: SearchDiagnostics, from: SearchDiagnostics): void;
22
22
  /**
23
23
  * Split a per-account search payload into its message-list portion and parsed
24
- * diagnostics. The AppleScript appends a trailer of the form:
24
+ * diagnostics. The AppleScript appends a trailer of the form (using the
25
+ * control-character separators defined above):
25
26
  *
26
- * <messages>|||DIAG|||timedOut=true|||F|||skipped=Foo (9000)|||M||||||F|||notSearched=Bar|||M|||
27
+ * <messages>{DIAG_MARKER}timedOut=true{DIAG_FIELD_SEP}skipped=Foo (9000){DIAG_ITEM_SEP}{DIAG_FIELD_SEP}notSearched=Bar{DIAG_ITEM_SEP}
27
28
  *
28
- * `skipped`/`notSearched` are `|||M|||`-separated mailbox names, each prefixed
29
+ * `skipped`/`notSearched` are DIAG_ITEM_SEP-separated mailbox names, each prefixed
29
30
  * with the account name on the way out so the aggregate result is unambiguous.
30
31
  *
31
32
  * Exported (pure, no Mail.app dependency) for unit testing — this is the logic
@@ -334,35 +335,48 @@ export declare class AppleMailManager {
334
335
  private moveMessageInternal;
335
336
  moveMessage(id: string, mailbox: string, account?: string): boolean;
336
337
  /**
337
- * Delete multiple messages at once.
338
+ * Run one operation over many message IDs in a SINGLE osascript invocation.
338
339
  *
339
- * @param ids - Array of message IDs to delete
340
- * @returns Array of results for each message
340
+ * Previously each batch method looped and called the per-id method, so a
341
+ * 100-id batch spawned 100 osascript processes — each one re-resolving
342
+ * accounts and walking the whole account→mailbox tree — all serialized
343
+ * through the gate (issue #31). This walks the tree exactly once: for each
344
+ * mailbox it probes the still-pending IDs with `whose id is` (indexed, so
345
+ * effectively free) and applies `operation` to any match, tracking found IDs
346
+ * so it can stop early once all are accounted for. Per-id outcomes come back
347
+ * as control-char-delimited `id<FS>status` records (status: `ok`,
348
+ * `notfound`, or `error:<msg>`), and results are returned in input order.
349
+ *
350
+ * `setup` runs once before the walk (used by move to resolve the destination);
351
+ * it may bail the whole batch by returning a `BATCH_FATAL`-prefixed string.
352
+ */
353
+ private runBatchOperation;
354
+ /**
355
+ * Delete multiple messages at once (single tree walk — see runBatchOperation).
341
356
  */
342
357
  batchDeleteMessages(ids: string[]): BatchOperationResult[];
343
358
  /**
344
- * Move multiple messages to a mailbox at once.
359
+ * Move multiple messages to a mailbox at once (single tree walk).
345
360
  *
346
- * @param ids - Array of message IDs to move
347
- * @param mailbox - Destination mailbox name
348
- * @param account - Account containing the destination mailbox
349
- * @returns Array of results for each message
361
+ * The destination is resolved once (account-scoped, ambiguity-aware a name
362
+ * matching more than one mailbox fails the whole batch rather than guessing),
363
+ * then every matched message is moved in the same walk.
350
364
  */
351
365
  batchMoveMessages(ids: string[], mailbox: string, account?: string): BatchOperationResult[];
352
366
  /**
353
- * Mark multiple messages as read at once.
367
+ * Mark multiple messages as read at once (single tree walk).
354
368
  */
355
369
  batchMarkAsRead(ids: string[]): BatchOperationResult[];
356
370
  /**
357
- * Mark multiple messages as unread at once.
371
+ * Mark multiple messages as unread at once (single tree walk).
358
372
  */
359
373
  batchMarkAsUnread(ids: string[]): BatchOperationResult[];
360
374
  /**
361
- * Flag multiple messages at once.
375
+ * Flag multiple messages at once (single tree walk).
362
376
  */
363
377
  batchFlagMessages(ids: string[]): BatchOperationResult[];
364
378
  /**
365
- * Unflag multiple messages at once.
379
+ * Unflag multiple messages at once (single tree walk).
366
380
  */
367
381
  batchUnflagMessages(ids: string[]): BatchOperationResult[];
368
382
  /**
@@ -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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"}
@@ -50,8 +50,27 @@ function getMailboxScanThreshold() {
50
50
  const SEARCH_ACCOUNT_BUDGET_SECONDS = 30;
51
51
  /** osascript process timeout for a per-account search (ms). */
52
52
  const SEARCH_ACCOUNT_TIMEOUT_MS = 45000;
53
- /** Separator between the message payload and the diagnostics trailer. */
54
- const DIAG_MARKER = "|||DIAG|||";
53
+ /**
54
+ * Result serialization separators (issue #30).
55
+ *
56
+ * AppleScript emits structured results as delimited strings that TS then splits.
57
+ * The original delimiters were printable triple-pipe tokens, so any field value
58
+ * that itself contained one — a subject, sender, attachment filename, or mailbox
59
+ * name with a triple-pipe in it — shifted every subsequent field and silently
60
+ * corrupted the parse. These are now ASCII control characters
61
+ * (Unit/Record/Group Separator) which cannot occur in mail field values, so the
62
+ * collision is structurally impossible. The same constant is used by the
63
+ * AppleScript emitter (interpolated into the script string) and the TS parser,
64
+ * so the two can never drift.
65
+ */
66
+ const FIELD_SEP = "\x1f"; // US — between fields within a record
67
+ const RECORD_SEP = "\x1e"; // RS — between records
68
+ const DIAG_MARKER = "\x1dDIAG\x1d"; // GS-wrapped — payload/diagnostics boundary
69
+ const DIAG_FIELD_SEP = "\x1dF\x1d"; // between diagnostics fields
70
+ const DIAG_ITEM_SEP = "\x1dM\x1d"; // between diagnostics list items
71
+ const CONTENT_MARKER = "\x1dCONTENT\x1d"; // subject/plain-text boundary
72
+ const HTML_MARKER = "\x1dHTML\x1d"; // plain-text/source boundary
73
+ const BATCH_FATAL = "\x1dFATAL\x1d"; // prefix for a whole-batch failure (e.g. bad destination)
55
74
  /**
56
75
  * Merge a per-account SearchDiagnostics into an aggregate (all-accounts) one.
57
76
  *
@@ -66,11 +85,12 @@ export function mergeSearchDiagnostics(into, from) {
66
85
  }
67
86
  /**
68
87
  * Split a per-account search payload into its message-list portion and parsed
69
- * diagnostics. The AppleScript appends a trailer of the form:
88
+ * diagnostics. The AppleScript appends a trailer of the form (using the
89
+ * control-character separators defined above):
70
90
  *
71
- * <messages>|||DIAG|||timedOut=true|||F|||skipped=Foo (9000)|||M||||||F|||notSearched=Bar|||M|||
91
+ * <messages>{DIAG_MARKER}timedOut=true{DIAG_FIELD_SEP}skipped=Foo (9000){DIAG_ITEM_SEP}{DIAG_FIELD_SEP}notSearched=Bar{DIAG_ITEM_SEP}
72
92
  *
73
- * `skipped`/`notSearched` are `|||M|||`-separated mailbox names, each prefixed
93
+ * `skipped`/`notSearched` are DIAG_ITEM_SEP-separated mailbox names, each prefixed
74
94
  * with the account name on the way out so the aggregate result is unambiguous.
75
95
  *
76
96
  * Exported (pure, no Mail.app dependency) for unit testing — this is the logic
@@ -87,13 +107,13 @@ export function splitSearchDiagnostics(output, account) {
87
107
  notSearchedMailboxes: [],
88
108
  };
89
109
  if (trailer) {
90
- const fields = trailer.split("|||F|||");
110
+ const fields = trailer.split(DIAG_FIELD_SEP);
91
111
  const getField = (key) => {
92
112
  const f = fields.find((x) => x.startsWith(`${key}=`));
93
113
  return f ? f.slice(key.length + 1) : "";
94
114
  };
95
115
  const splitList = (raw) => raw
96
- .split("|||M|||")
116
+ .split(DIAG_ITEM_SEP)
97
117
  .map((s) => s.trim())
98
118
  .filter((s) => s.length > 0);
99
119
  diagnostics.skippedLargeMailboxes = splitList(getField("skipped")).map((mb) => `${account} / ${mb}`);
@@ -521,17 +541,17 @@ export class AppleMailManager {
521
541
  set msgDateStr to ${AS_DATE_TO_STRING}
522
542
  set msgRead to read status of msg as string
523
543
  set msgFlagged to flagged status of msg as string
524
- if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
525
- set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDateStr & "|||" & msgRead & "|||" & msgFlagged
544
+ if msgCount > 0 then set outputText to outputText & "${RECORD_SEP}"
545
+ set outputText to outputText & msgId & "${FIELD_SEP}" & msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDateStr & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged
526
546
  set msgCount to msgCount + 1
527
547
  ${dateFilter ? "end if" : ""}
528
548
  end try
529
549
  end repeat
530
550
  on error _errMsg number _errNum
531
551
  set _timedOut to true
532
- set _notSearched to "${escapeForAppleScript(targetMailbox)}|||M|||"
552
+ set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
533
553
  end try
534
- return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "|||F|||skipped=|||F|||notSearched=" & _notSearched
554
+ return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=${DIAG_FIELD_SEP}notSearched=" & _notSearched
535
555
  `;
536
556
  }
537
557
  else {
@@ -556,7 +576,7 @@ export class AppleMailManager {
556
576
  end try
557
577
  if ((current date) - _startedAt) > ${SEARCH_ACCOUNT_BUDGET_SECONDS} then
558
578
  set _timedOut to true
559
- set _notSearched to _notSearched & mbName & "|||M|||"
579
+ set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
560
580
  else
561
581
  set mbCount to 0
562
582
  try
@@ -564,7 +584,7 @@ export class AppleMailManager {
564
584
  end try
565
585
  if (${scanGuard}) then
566
586
  set _timedOut to true
567
- set _skipped to _skipped & mbName & " (" & (mbCount as string) & ")|||M|||"
587
+ set _skipped to _skipped & mbName & " (" & (mbCount as string) & ")${DIAG_ITEM_SEP}"
568
588
  else
569
589
  try
570
590
  set allMessages to messages of mb ${searchCondition}
@@ -581,8 +601,8 @@ export class AppleMailManager {
581
601
  set msgDateStr to ${AS_DATE_TO_STRING}
582
602
  set msgRead to read status of msg as string
583
603
  set msgFlagged to flagged status of msg as string
584
- if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
585
- set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDateStr & "|||" & msgRead & "|||" & msgFlagged & "|||" & mbName
604
+ if msgCount > 0 then set outputText to outputText & "${RECORD_SEP}"
605
+ set outputText to outputText & msgId & "${FIELD_SEP}" & msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDateStr & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & mbName
586
606
  set msgCount to msgCount + 1
587
607
  ${dateFilter ? "end if" : ""}
588
608
  end if
@@ -590,12 +610,12 @@ export class AppleMailManager {
590
610
  end repeat
591
611
  on error _errMsg number _errNum
592
612
  set _timedOut to true
593
- set _notSearched to _notSearched & mbName & "|||M|||"
613
+ set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
594
614
  end try
595
615
  end if
596
616
  end if
597
617
  end repeat
598
- return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "|||F|||skipped=" & _skipped & "|||F|||notSearched=" & _notSearched
618
+ return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=" & _skipped & "${DIAG_FIELD_SEP}notSearched=" & _notSearched
599
619
  `;
600
620
  }
601
621
  const script = buildAccountScopedScript(targetAccount, searchCommand);
@@ -668,7 +688,7 @@ export class AppleMailManager {
668
688
  if rawSrc contains "Content-Disposition: attachment" then set hasAtt to "true"
669
689
  end try
670
690
  end if
671
- return msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & msgJunk & "|||" & msgDeleted & "|||" & msgMailbox & "|||" & msgAccount & "|||" & hasAtt
691
+ return msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDate & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & msgJunk & "${FIELD_SEP}" & msgDeleted & "${FIELD_SEP}" & msgMailbox & "${FIELD_SEP}" & msgAccount & "${FIELD_SEP}" & hasAtt
672
692
  end if
673
693
  end try
674
694
  end repeat
@@ -683,7 +703,7 @@ export class AppleMailManager {
683
703
  console.error(`Failed to get message ${id}: ${result.error}`);
684
704
  return null;
685
705
  }
686
- const parts = result.output.split("|||");
706
+ const parts = result.output.split(FIELD_SEP);
687
707
  if (parts.length < 9)
688
708
  return null;
689
709
  return {
@@ -719,7 +739,7 @@ export class AppleMailManager {
719
739
  try
720
740
  set htmlContent to source of msg
721
741
  end try
722
- return msgSubject & "|||CONTENT|||" & msgContent & "|||HTML|||" & htmlContent
742
+ return msgSubject & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlContent
723
743
  end if
724
744
  end try
725
745
  end repeat
@@ -734,10 +754,10 @@ export class AppleMailManager {
734
754
  console.error(`Failed to get message content: ${result.error}`);
735
755
  return null;
736
756
  }
737
- const htmlSplit = result.output.split("|||HTML|||");
757
+ const htmlSplit = result.output.split(HTML_MARKER);
738
758
  const contentPart = htmlSplit[0];
739
759
  const htmlContent = htmlSplit.length > 1 ? htmlSplit[1] : undefined;
740
- const parts = contentPart.split("|||CONTENT|||");
760
+ const parts = contentPart.split(CONTENT_MARKER);
741
761
  if (parts.length < 2)
742
762
  return null;
743
763
  return {
@@ -859,17 +879,17 @@ export class AppleMailManager {
859
879
  try
860
880
  if (count of mail attachments of msg) > 0 then set msgHasAtt to "true"
861
881
  end try
862
- if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
863
- set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & msgHasAtt
882
+ if msgCount > 0 then set outputText to outputText & "${RECORD_SEP}"
883
+ set outputText to outputText & msgId & "${FIELD_SEP}" & msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDate & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & msgHasAtt
864
884
  set msgCount to msgCount + 1
865
885
  end if
866
886
  end try
867
887
  end repeat
868
888
  on error _errMsg number _errNum
869
889
  set _timedOut to true
870
- set _notSearched to "${escapeForAppleScript(targetMailbox)}|||M|||"
890
+ set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
871
891
  end try
872
- return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "|||F|||skipped=|||F|||notSearched=" & _notSearched
892
+ return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=${DIAG_FIELD_SEP}notSearched=" & _notSearched
873
893
  `;
874
894
  }
875
895
  else {
@@ -893,7 +913,7 @@ export class AppleMailManager {
893
913
  end try
894
914
  if ((current date) - _startedAt) > ${SEARCH_ACCOUNT_BUDGET_SECONDS} then
895
915
  set _timedOut to true
896
- set _notSearched to _notSearched & mbName & "|||M|||"
916
+ set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
897
917
  else
898
918
  set mbCount to 0
899
919
  try
@@ -901,7 +921,7 @@ export class AppleMailManager {
901
921
  end try
902
922
  if (${scanGuard}) then
903
923
  set _timedOut to true
904
- set _skipped to _skipped & mbName & " (" & (mbCount as string) & ")|||M|||"
924
+ set _skipped to _skipped & mbName & " (" & (mbCount as string) & ")${DIAG_ITEM_SEP}"
905
925
  else
906
926
  try
907
927
  repeat with msg in messages of mb ${fromFilter}
@@ -923,8 +943,8 @@ export class AppleMailManager {
923
943
  try
924
944
  if (count of mail attachments of msg) > 0 then set msgHasAtt to "true"
925
945
  end try
926
- if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
927
- set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & mbName & "|||" & msgHasAtt
946
+ if msgCount > 0 then set outputText to outputText & "${RECORD_SEP}"
947
+ set outputText to outputText & msgId & "${FIELD_SEP}" & msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDate & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & mbName & "${FIELD_SEP}" & msgHasAtt
928
948
  set msgCount to msgCount + 1
929
949
  end if
930
950
  end if
@@ -932,12 +952,12 @@ export class AppleMailManager {
932
952
  end repeat
933
953
  on error _errMsg number _errNum
934
954
  set _timedOut to true
935
- set _notSearched to _notSearched & mbName & "|||M|||"
955
+ set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
936
956
  end try
937
957
  end if
938
958
  end if
939
959
  end repeat
940
- return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "|||F|||skipped=" & _skipped & "|||F|||notSearched=" & _notSearched
960
+ return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=" & _skipped & "${DIAG_FIELD_SEP}notSearched=" & _notSearched
941
961
  `;
942
962
  }
943
963
  const script = buildAccountScopedScript(targetAccount, listCommand);
@@ -969,10 +989,10 @@ export class AppleMailManager {
969
989
  * limitation). Use getMessage or list-attachments for authoritative info.
970
990
  */
971
991
  parseMessageList(output, mailbox, account) {
972
- const items = output.split("|||ITEM|||");
992
+ const items = output.split(RECORD_SEP);
973
993
  const messages = [];
974
994
  for (const item of items) {
975
- const parts = item.split("|||");
995
+ const parts = item.split(FIELD_SEP);
976
996
  if (parts.length < 6)
977
997
  continue;
978
998
  let msgMailbox = mailbox;
@@ -1447,90 +1467,174 @@ export class AppleMailManager {
1447
1467
  // Batch Operations
1448
1468
  // ===========================================================================
1449
1469
  /**
1450
- * Delete multiple messages at once.
1470
+ * Run one operation over many message IDs in a SINGLE osascript invocation.
1451
1471
  *
1452
- * @param ids - Array of message IDs to delete
1453
- * @returns Array of results for each message
1472
+ * Previously each batch method looped and called the per-id method, so a
1473
+ * 100-id batch spawned 100 osascript processes — each one re-resolving
1474
+ * accounts and walking the whole account→mailbox tree — all serialized
1475
+ * through the gate (issue #31). This walks the tree exactly once: for each
1476
+ * mailbox it probes the still-pending IDs with `whose id is` (indexed, so
1477
+ * effectively free) and applies `operation` to any match, tracking found IDs
1478
+ * so it can stop early once all are accounted for. Per-id outcomes come back
1479
+ * as control-char-delimited `id<FS>status` records (status: `ok`,
1480
+ * `notfound`, or `error:<msg>`), and results are returned in input order.
1481
+ *
1482
+ * `setup` runs once before the walk (used by move to resolve the destination);
1483
+ * it may bail the whole batch by returning a `BATCH_FATAL`-prefixed string.
1454
1484
  */
1455
- batchDeleteMessages(ids) {
1456
- const results = [];
1485
+ runBatchOperation(ids, operation, setup = "") {
1486
+ // Keep the numeric IDs paired with their original string form and 1-based
1487
+ // position. The AppleScript reports outcomes by POSITION, not by id: a Mail
1488
+ // id large enough to exceed AppleScript's 2^29 integer range coerces to
1489
+ // scientific notation under `as string` (999999999 -> "9.99999999E+8"), so
1490
+ // echoing the id back can't be matched to the input. Positions are always
1491
+ // small integers, so they round-trip cleanly.
1492
+ const valid = [];
1457
1493
  for (const id of ids) {
1458
- const success = this.deleteMessage(id);
1459
- results.push({
1460
- id,
1461
- success,
1462
- error: success ? undefined : "Failed to delete message",
1463
- });
1494
+ const num = Number(id);
1495
+ if (Number.isFinite(num))
1496
+ valid.push({ id, num });
1464
1497
  }
1465
- return results;
1498
+ if (valid.length === 0) {
1499
+ return ids.map((id) => ({ id, success: false, error: "Invalid message ID" }));
1500
+ }
1501
+ const script = buildAppLevelScript(`
1502
+ try
1503
+ ${setup}
1504
+ set _out to ""
1505
+ set _done to {}
1506
+ set _ids to {${valid.map((v) => v.num).join(", ")}}
1507
+ set _total to count of _ids
1508
+ repeat with acct in accounts
1509
+ if (count of _done) is _total then exit repeat
1510
+ repeat with mb in (mailboxes of acct)
1511
+ if (count of _done) is _total then exit repeat
1512
+ repeat with _idx from 1 to _total
1513
+ if _idx is not in _done then
1514
+ set _theId to item _idx of _ids
1515
+ try
1516
+ set _m to (messages of mb whose id is _theId)
1517
+ if (count of _m) > 0 then
1518
+ set _msg to item 1 of _m
1519
+ ${operation}
1520
+ set end of _done to _idx
1521
+ set _out to _out & (_idx as string) & "${FIELD_SEP}ok${RECORD_SEP}"
1522
+ end if
1523
+ on error _e
1524
+ set end of _done to _idx
1525
+ set _out to _out & (_idx as string) & "${FIELD_SEP}error:" & _e & "${RECORD_SEP}"
1526
+ end try
1527
+ end if
1528
+ end repeat
1529
+ end repeat
1530
+ end repeat
1531
+ repeat with _idx from 1 to _total
1532
+ if _idx is not in _done then set _out to _out & (_idx as string) & "${FIELD_SEP}notfound${RECORD_SEP}"
1533
+ end repeat
1534
+ return _out
1535
+ on error errMsg
1536
+ return "${BATCH_FATAL}" & errMsg
1537
+ end try
1538
+ `);
1539
+ // Generous timeout: one walk over the tree with indexed id probes. Scale a
1540
+ // little with batch size, capped.
1541
+ const timeoutMs = Math.min(180000, 60000 + valid.length * 500);
1542
+ const result = executeAppleScript(script, { timeoutMs });
1543
+ if (!result.success) {
1544
+ const err = result.error || "Batch operation failed";
1545
+ return ids.map((id) => ({ id, success: false, error: err }));
1546
+ }
1547
+ if (result.output.startsWith(BATCH_FATAL)) {
1548
+ const err = result.output.slice(BATCH_FATAL.length);
1549
+ return ids.map((id) => ({ id, success: false, error: err }));
1550
+ }
1551
+ // Map by-position outcomes back to the original id strings.
1552
+ const byId = new Map();
1553
+ for (const rec of result.output.split(RECORD_SEP)) {
1554
+ if (!rec)
1555
+ continue;
1556
+ const sep = rec.indexOf(FIELD_SEP);
1557
+ if (sep < 0)
1558
+ continue;
1559
+ const pos = Number(rec.slice(0, sep));
1560
+ const status = rec.slice(sep + FIELD_SEP.length);
1561
+ const entry = valid[pos - 1];
1562
+ if (!entry)
1563
+ continue;
1564
+ const id = entry.id;
1565
+ if (status === "ok") {
1566
+ byId.set(id, { id, success: true });
1567
+ }
1568
+ else if (status === "notfound") {
1569
+ byId.set(id, { id, success: false, error: "Message not found" });
1570
+ }
1571
+ else if (status.startsWith("error:")) {
1572
+ byId.set(id, { id, success: false, error: status.slice("error:".length) });
1573
+ }
1574
+ else {
1575
+ byId.set(id, { id, success: false, error: status || "Unknown error" });
1576
+ }
1577
+ }
1578
+ return ids.map((id) => byId.get(id) ??
1579
+ (Number.isFinite(Number(id))
1580
+ ? { id, success: false, error: "No result returned" }
1581
+ : { id, success: false, error: "Invalid message ID" }));
1582
+ }
1583
+ /**
1584
+ * Delete multiple messages at once (single tree walk — see runBatchOperation).
1585
+ */
1586
+ batchDeleteMessages(ids) {
1587
+ return this.runBatchOperation(ids, "delete _msg");
1466
1588
  }
1467
1589
  /**
1468
- * Move multiple messages to a mailbox at once.
1590
+ * Move multiple messages to a mailbox at once (single tree walk).
1469
1591
  *
1470
- * @param ids - Array of message IDs to move
1471
- * @param mailbox - Destination mailbox name
1472
- * @param account - Account containing the destination mailbox
1473
- * @returns Array of results for each message
1592
+ * The destination is resolved once (account-scoped, ambiguity-aware a name
1593
+ * matching more than one mailbox fails the whole batch rather than guessing),
1594
+ * then every matched message is moved in the same walk.
1474
1595
  */
1475
1596
  batchMoveMessages(ids, mailbox, account) {
1476
- const results = [];
1477
- for (const id of ids) {
1478
- const { success, error } = this.moveMessageInternal(id, mailbox, account);
1479
- results.push({
1480
- id,
1481
- success,
1482
- error: success ? undefined : error || "Failed to move message",
1483
- });
1484
- }
1485
- return results;
1597
+ const targetAccount = this.resolveAccount(account);
1598
+ const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
1599
+ const safeMailbox = escapeForAppleScript(targetMailbox);
1600
+ const safeAccount = escapeForAppleScript(targetAccount);
1601
+ // Resolved once, before the walk. `mailboxes of account` is already flat
1602
+ // (includes nested mailboxes by path), so we match by exact name and use the
1603
+ // reference directly. A bad/ambiguous destination fails the whole batch.
1604
+ const setup = `
1605
+ set destName to "${safeMailbox}"
1606
+ set destMatches to {}
1607
+ repeat with _dmb in (mailboxes of account "${safeAccount}")
1608
+ if (name of _dmb) is destName then set end of destMatches to _dmb
1609
+ end repeat
1610
+ if (count of destMatches) is 0 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" not found in account \\"${safeAccount}\\""
1611
+ if (count of destMatches) > 1 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" is ambiguous (" & (count of destMatches) & " matches) in account \\"${safeAccount}\\"; move by full path"
1612
+ set destMailbox to item 1 of destMatches`;
1613
+ return this.runBatchOperation(ids, "move _msg to destMailbox", setup);
1486
1614
  }
1487
1615
  /**
1488
- * Mark multiple messages as read at once.
1616
+ * Mark multiple messages as read at once (single tree walk).
1489
1617
  */
1490
1618
  batchMarkAsRead(ids) {
1491
- const results = [];
1492
- for (const id of ids) {
1493
- const success = this.markAsRead(id);
1494
- results.push({ id, success, error: success ? undefined : "Failed to mark message as read" });
1495
- }
1496
- return results;
1619
+ return this.runBatchOperation(ids, "set read status of _msg to true");
1497
1620
  }
1498
1621
  /**
1499
- * Mark multiple messages as unread at once.
1622
+ * Mark multiple messages as unread at once (single tree walk).
1500
1623
  */
1501
1624
  batchMarkAsUnread(ids) {
1502
- const results = [];
1503
- for (const id of ids) {
1504
- const success = this.markAsUnread(id);
1505
- results.push({
1506
- id,
1507
- success,
1508
- error: success ? undefined : "Failed to mark message as unread",
1509
- });
1510
- }
1511
- return results;
1625
+ return this.runBatchOperation(ids, "set read status of _msg to false");
1512
1626
  }
1513
1627
  /**
1514
- * Flag multiple messages at once.
1628
+ * Flag multiple messages at once (single tree walk).
1515
1629
  */
1516
1630
  batchFlagMessages(ids) {
1517
- const results = [];
1518
- for (const id of ids) {
1519
- const success = this.flagMessage(id);
1520
- results.push({ id, success, error: success ? undefined : "Failed to flag message" });
1521
- }
1522
- return results;
1631
+ return this.runBatchOperation(ids, "set flagged status of _msg to true");
1523
1632
  }
1524
1633
  /**
1525
- * Unflag multiple messages at once.
1634
+ * Unflag multiple messages at once (single tree walk).
1526
1635
  */
1527
1636
  batchUnflagMessages(ids) {
1528
- const results = [];
1529
- for (const id of ids) {
1530
- const success = this.unflagMessage(id);
1531
- results.push({ id, success, error: success ? undefined : "Failed to unflag message" });
1532
- }
1533
- return results;
1637
+ return this.runBatchOperation(ids, "set flagged status of _msg to false");
1534
1638
  }
1535
1639
  /**
1536
1640
  * List attachments for a message.
@@ -1553,8 +1657,8 @@ export class AppleMailManager {
1553
1657
  set attName to name of att
1554
1658
  set attType to MIME type of att
1555
1659
  set attSize to file size of att as string
1556
- if attCount > 0 then set outputText to outputText & "|||ITEM|||"
1557
- set outputText to outputText & attName & "|||" & attType & "|||" & attSize
1660
+ if attCount > 0 then set outputText to outputText & "${RECORD_SEP}"
1661
+ set outputText to outputText & attName & "${FIELD_SEP}" & attType & "${FIELD_SEP}" & attSize
1558
1662
  set attCount to attCount + 1
1559
1663
  end repeat
1560
1664
  return outputText
@@ -1569,10 +1673,10 @@ export class AppleMailManager {
1569
1673
  `);
1570
1674
  const result = executeAppleScript(script, { timeoutMs: 60000 });
1571
1675
  if (result.success && result.output.trim()) {
1572
- const items = result.output.split("|||ITEM|||");
1676
+ const items = result.output.split(RECORD_SEP);
1573
1677
  const attachments = [];
1574
1678
  for (const item of items) {
1575
- const parts = item.split("|||");
1679
+ const parts = item.split(FIELD_SEP);
1576
1680
  if (parts.length < 3)
1577
1681
  continue;
1578
1682
  attachments.push({
@@ -1690,9 +1794,9 @@ export class AppleMailManager {
1690
1794
  set mbName to name of mb
1691
1795
  set mbUnread to unread count of mb
1692
1796
  set mbCount to count of messages of mb
1693
- set end of mailboxList to mbName & "|||" & mbUnread & "|||" & mbCount
1797
+ set end of mailboxList to mbName & "${FIELD_SEP}" & mbUnread & "${FIELD_SEP}" & mbCount
1694
1798
  end repeat
1695
- set AppleScript's text item delimiters to "|||ITEM|||"
1799
+ set AppleScript's text item delimiters to "${RECORD_SEP}"
1696
1800
  return mailboxList as text
1697
1801
  `;
1698
1802
  const script = buildAccountScopedScript(targetAccount, listCommand);
@@ -1703,10 +1807,10 @@ export class AppleMailManager {
1703
1807
  }
1704
1808
  if (!result.output.trim())
1705
1809
  return [];
1706
- const items = result.output.split("|||ITEM|||");
1810
+ const items = result.output.split(RECORD_SEP);
1707
1811
  const mailboxes = [];
1708
1812
  for (const item of items) {
1709
- const parts = item.split("|||");
1813
+ const parts = item.split(FIELD_SEP);
1710
1814
  if (parts.length < 3)
1711
1815
  continue;
1712
1816
  mailboxes.push({
@@ -1854,9 +1958,9 @@ export class AppleMailManager {
1854
1958
  if (count of acctEmail) > 0 then
1855
1959
  set emailStr to item 1 of acctEmail
1856
1960
  end if
1857
- set end of accountList to acctName & "|||" & emailStr & "|||" & acctEnabled
1961
+ set end of accountList to acctName & "${FIELD_SEP}" & emailStr & "${FIELD_SEP}" & acctEnabled
1858
1962
  end repeat
1859
- set AppleScript's text item delimiters to "|||ITEM|||"
1963
+ set AppleScript's text item delimiters to "${RECORD_SEP}"
1860
1964
  return accountList as text
1861
1965
  `);
1862
1966
  const result = executeAppleScript(script);
@@ -1866,10 +1970,10 @@ export class AppleMailManager {
1866
1970
  }
1867
1971
  if (!result.output.trim())
1868
1972
  return [];
1869
- const items = result.output.split("|||ITEM|||");
1973
+ const items = result.output.split(RECORD_SEP);
1870
1974
  const accounts = [];
1871
1975
  for (const item of items) {
1872
- const parts = item.split("|||");
1976
+ const parts = item.split(FIELD_SEP);
1873
1977
  if (parts.length < 3)
1874
1978
  continue;
1875
1979
  accounts.push({
@@ -1910,19 +2014,19 @@ export class AppleMailManager {
1910
2014
  repeat with r in rules
1911
2015
  set ruleName to name of r
1912
2016
  set ruleEnabled to enabled of r
1913
- set end of ruleList to ruleName & "|||" & (ruleEnabled as string)
2017
+ set end of ruleList to ruleName & "${FIELD_SEP}" & (ruleEnabled as string)
1914
2018
  end repeat
1915
- set AppleScript's text item delimiters to "|||ITEM|||"
2019
+ set AppleScript's text item delimiters to "${RECORD_SEP}"
1916
2020
  return ruleList as text
1917
2021
  `);
1918
2022
  const result = executeAppleScript(script);
1919
2023
  if (!result.success || !result.output.trim()) {
1920
2024
  return [];
1921
2025
  }
1922
- const items = result.output.split("|||ITEM|||");
2026
+ const items = result.output.split(RECORD_SEP);
1923
2027
  const rules = [];
1924
2028
  for (const item of items) {
1925
- const parts = item.split("|||");
2029
+ const parts = item.split(FIELD_SEP);
1926
2030
  if (parts.length < 2)
1927
2031
  continue;
1928
2032
  rules.push({
@@ -1987,11 +2091,11 @@ export class AppleMailManager {
1987
2091
  if pPhones is not "" then set pPhones to pPhones & ","
1988
2092
  set pPhones to pPhones & (value of ph)
1989
2093
  end repeat
1990
- set end of matchedContacts to pName & "|||" & pEmails & "|||" & pPhones
2094
+ set end of matchedContacts to pName & "${FIELD_SEP}" & pEmails & "${FIELD_SEP}" & pPhones
1991
2095
  end if
1992
2096
  end repeat
1993
2097
 
1994
- set AppleScript's text item delimiters to "|||ITEM|||"
2098
+ set AppleScript's text item delimiters to "${RECORD_SEP}"
1995
2099
  return matchedContacts as text
1996
2100
  end tell
1997
2101
  `;
@@ -1999,10 +2103,10 @@ export class AppleMailManager {
1999
2103
  if (!result.success || !result.output.trim()) {
2000
2104
  return [];
2001
2105
  }
2002
- const items = result.output.split("|||ITEM|||");
2106
+ const items = result.output.split(RECORD_SEP);
2003
2107
  const contacts = [];
2004
2108
  for (const item of items) {
2005
- const parts = item.split("|||");
2109
+ const parts = item.split(FIELD_SEP);
2006
2110
  if (parts.length < 3)
2007
2111
  continue;
2008
2112
  contacts.push({
@@ -2225,14 +2329,14 @@ export class AppleMailManager {
2225
2329
  end try
2226
2330
  end repeat
2227
2331
 
2228
- return (last24h as string) & "|||" & (last7d as string) & "|||" & (last30d as string)
2332
+ return (last24h as string) & "${FIELD_SEP}" & (last7d as string) & "${FIELD_SEP}" & (last30d as string)
2229
2333
  `);
2230
2334
  const result = executeAppleScript(script, { timeoutMs: 60000 });
2231
2335
  if (!result.success || !result.output.trim()) {
2232
2336
  console.error(`Failed to get recently received stats: ${result.error}`);
2233
2337
  return { last24h: 0, last7d: 0, last30d: 0 };
2234
2338
  }
2235
- const parts = result.output.split("|||");
2339
+ const parts = result.output.split(FIELD_SEP);
2236
2340
  if (parts.length < 3) {
2237
2341
  return { last24h: 0, last7d: 0, last30d: 0 };
2238
2342
  }
@@ -2276,7 +2380,7 @@ export class AppleMailManager {
2276
2380
  set totalMailboxes to totalMailboxes + (count of mailboxes of acct)
2277
2381
  end repeat
2278
2382
 
2279
- return "running|||" & accountCount & "|||" & totalMailboxes
2383
+ return "running${FIELD_SEP}" & accountCount & "${FIELD_SEP}" & totalMailboxes
2280
2384
  `);
2281
2385
  const result = executeAppleScript(script);
2282
2386
  if (!result.success) {
@@ -2298,7 +2402,7 @@ export class AppleMailManager {
2298
2402
  };
2299
2403
  }
2300
2404
  // Parse the response
2301
- const parts = result.output.split("|||");
2405
+ const parts = result.output.split(FIELD_SEP);
2302
2406
  const isRunning = parts[0] === "running";
2303
2407
  const accountCount = parseInt(parts[1]) || 0;
2304
2408
  // Mail.app is running with accounts configured - assume sync is active
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
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",