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
|
|
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
|
|
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
|
-
*
|
|
338
|
+
* Run one operation over many message IDs in a SINGLE osascript invocation.
|
|
338
339
|
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
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
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
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":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,OAAO,EACP,OAAO,EACP,UAAU,EACV,iBAAiB,EACjB,SAAS,EAET,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,QAAQ,EACR,OAAO,EACP,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,YAAY,EACb,MAAM,YAAY,CAAC;
|
|
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
|
-
/**
|
|
54
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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 & "
|
|
525
|
-
set outputText to outputText & msgId & "
|
|
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)}
|
|
552
|
+
set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
|
|
533
553
|
end try
|
|
534
|
-
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "
|
|
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 & "
|
|
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) & ")
|
|
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 & "
|
|
585
|
-
set outputText to outputText & msgId & "
|
|
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 & "
|
|
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) & "
|
|
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 & "
|
|
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 & "
|
|
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(
|
|
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(
|
|
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 & "
|
|
863
|
-
set outputText to outputText & msgId & "
|
|
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)}
|
|
890
|
+
set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
|
|
871
891
|
end try
|
|
872
|
-
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "
|
|
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 & "
|
|
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) & ")
|
|
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 & "
|
|
927
|
-
set outputText to outputText & msgId & "
|
|
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 & "
|
|
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) & "
|
|
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(
|
|
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
|
-
*
|
|
1470
|
+
* Run one operation over many message IDs in a SINGLE osascript invocation.
|
|
1451
1471
|
*
|
|
1452
|
-
*
|
|
1453
|
-
*
|
|
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
|
-
|
|
1456
|
-
|
|
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
|
|
1459
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
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
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
}
|
|
1485
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 & "
|
|
1557
|
-
set outputText to outputText & attName & "
|
|
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(
|
|
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 & "
|
|
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 "
|
|
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(
|
|
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 & "
|
|
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 "
|
|
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(
|
|
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 & "
|
|
2017
|
+
set end of ruleList to ruleName & "${FIELD_SEP}" & (ruleEnabled as string)
|
|
1914
2018
|
end repeat
|
|
1915
|
-
set AppleScript's text item delimiters to "
|
|
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(
|
|
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 & "
|
|
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 "
|
|
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(
|
|
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) & "
|
|
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
|
|
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
|