apple-mail-mcp 1.6.3 → 1.6.5
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/build/index.js
CHANGED
|
@@ -85,6 +85,28 @@ function errorResponse(message) {
|
|
|
85
85
|
isError: true,
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Render a partial-coverage warning from search/list diagnostics, so a caller
|
|
90
|
+
* never mistakes an incomplete scan for a confirmed "no matches" (#24/#29).
|
|
91
|
+
* Returns "" when coverage was complete.
|
|
92
|
+
*/
|
|
93
|
+
function partialCoverageBlock(diagnostics) {
|
|
94
|
+
const notes = [];
|
|
95
|
+
if (diagnostics.timedOutAccounts.length > 0) {
|
|
96
|
+
notes.push(`timed out (no results) for account(s): ${diagnostics.timedOutAccounts.join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
if (diagnostics.skippedLargeMailboxes.length > 0) {
|
|
99
|
+
notes.push(`skipped mailbox(es) too large to scan via AppleScript: ${diagnostics.skippedLargeMailboxes.join(", ")} — scope with \`mailbox\` (+ a \`dateFrom\`/\`dateTo\` window for search) to reach them`);
|
|
100
|
+
}
|
|
101
|
+
if (diagnostics.notSearchedMailboxes.length > 0) {
|
|
102
|
+
notes.push(`could not finish scanning mailbox(es): ${diagnostics.notSearchedMailboxes.join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
if (notes.length === 0)
|
|
105
|
+
return "";
|
|
106
|
+
return `\n\n⚠️ Partial results — this is NOT a confirmed "no such mail":\n${notes
|
|
107
|
+
.map((n) => ` - ${n}`)
|
|
108
|
+
.join("\n")}`;
|
|
109
|
+
}
|
|
88
110
|
/**
|
|
89
111
|
* Serial execution gate for AppleScript-backed tool calls (issue #11).
|
|
90
112
|
*
|
|
@@ -137,24 +159,7 @@ server.tool("search-messages", {
|
|
|
137
159
|
limit: z.number().optional().describe("Maximum number of results (default: 50)"),
|
|
138
160
|
}, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
|
|
139
161
|
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged);
|
|
140
|
-
|
|
141
|
-
// caller never mistakes an incomplete search for a confirmed "no matches"
|
|
142
|
-
// (issue #24).
|
|
143
|
-
const coverageNotes = [];
|
|
144
|
-
if (diagnostics.timedOutAccounts.length > 0) {
|
|
145
|
-
coverageNotes.push(`timed out (no results) for account(s): ${diagnostics.timedOutAccounts.join(", ")}`);
|
|
146
|
-
}
|
|
147
|
-
if (diagnostics.skippedLargeMailboxes.length > 0) {
|
|
148
|
-
coverageNotes.push(`skipped mailbox(es) too large to search via AppleScript: ${diagnostics.skippedLargeMailboxes.join(", ")} — scope the search with \`mailbox\` + a \`dateFrom\`/\`dateTo\` window to target them`);
|
|
149
|
-
}
|
|
150
|
-
if (diagnostics.notSearchedMailboxes.length > 0) {
|
|
151
|
-
coverageNotes.push(`could not finish searching mailbox(es): ${diagnostics.notSearchedMailboxes.join(", ")}`);
|
|
152
|
-
}
|
|
153
|
-
const coverageBlock = coverageNotes.length > 0
|
|
154
|
-
? `\n\n⚠️ Partial results — this is NOT a confirmed "no such mail":\n${coverageNotes
|
|
155
|
-
.map((n) => ` - ${n}`)
|
|
156
|
-
.join("\n")}`
|
|
157
|
-
: "";
|
|
162
|
+
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
158
163
|
if (messages.length === 0) {
|
|
159
164
|
const base = diagnostics.partial
|
|
160
165
|
? "No messages found in the portions that were searched."
|
|
@@ -192,14 +197,18 @@ server.tool("list-messages", {
|
|
|
192
197
|
from: z.string().optional().describe("Filter by sender email address or name"),
|
|
193
198
|
unreadOnly: z.boolean().optional().describe("Only show unread messages"),
|
|
194
199
|
}, withErrorHandling(({ mailbox, account, limit = 50, offset = 0, from }) => {
|
|
195
|
-
const messages = mailManager.
|
|
200
|
+
const { messages, diagnostics } = mailManager.listMessagesWithDiagnostics(mailbox, account, limit, from, offset);
|
|
201
|
+
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
196
202
|
if (messages.length === 0) {
|
|
197
|
-
|
|
203
|
+
const base = diagnostics.partial
|
|
204
|
+
? "No messages found in the portions that were listed."
|
|
205
|
+
: "No messages found";
|
|
206
|
+
return successResponse(`${base}${coverageBlock}`);
|
|
198
207
|
}
|
|
199
208
|
const messageList = messages
|
|
200
209
|
.map((m) => ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender})`)
|
|
201
210
|
.join("\n");
|
|
202
|
-
return successResponse(`Found ${messages.length} message(s):\n${messageList}`);
|
|
211
|
+
return successResponse(`Found ${messages.length} message(s):\n${messageList}${coverageBlock}`);
|
|
203
212
|
}, "Error listing messages"));
|
|
204
213
|
// --- send-email ---
|
|
205
214
|
server.tool("send-email", {
|
|
@@ -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
|
|
@@ -203,6 +204,19 @@ export declare class AppleMailManager {
|
|
|
203
204
|
* @returns Array of messages
|
|
204
205
|
*/
|
|
205
206
|
listMessages(mailbox?: string, account?: string, limit?: number, from?: string, offset?: number): Message[];
|
|
207
|
+
/**
|
|
208
|
+
* List messages, returning matches plus coverage diagnostics.
|
|
209
|
+
*
|
|
210
|
+
* Like `searchMessages`, the unscoped (all-mailboxes) path used to iterate
|
|
211
|
+
* `messages of mb` over every mailbox with a swallowing per-mailbox `try`,
|
|
212
|
+
* so a large IMAP/Gmail mailbox timed out and the method returned `[]` — a
|
|
213
|
+
* false "No messages found." This applies the same #24 discipline: skip
|
|
214
|
+
* mailboxes above the scan threshold (reported), enforce a per-account
|
|
215
|
+
* wall-clock budget, capture per-mailbox timeouts, and surface all of it as a
|
|
216
|
+
* partial result. (by-id lookups don't need this — `whose id is` is indexed
|
|
217
|
+
* and returns instantly even on a 44k-message mailbox.)
|
|
218
|
+
*/
|
|
219
|
+
listMessagesWithDiagnostics(mailbox?: string, account?: string, limit?: number, from?: string, offset?: number): SearchResult;
|
|
206
220
|
/**
|
|
207
221
|
* Parse message list output from AppleScript.
|
|
208
222
|
*
|
|
@@ -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":"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;AA0DpB;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAK7F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,iBAAiB,CAAA;CAAE,CAsCrD;AAoFD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,CAWrE;AA2CD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAoB5E;AAED,qBAAa,gBAAgB;IAC3B;;OAEG;IACH,OAAO,CAAC,cAAc,CAAuB;IAE7C;;;;OAIG;IACH,OAAO,CAAC,KAAK,CAGX;IAEF,8CAA8C;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IAEvC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAUzB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAKvB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAwCtB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,cAAc;IAyCtB;;;;;;;;OAQG;IACH,cAAc,CACZ,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,SAAK,EACV,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,OAAO,EAChB,SAAS,CAAC,EAAE,OAAO,GAClB,OAAO,EAAE;IAeZ;;;;;;;;;;;;;;;;;OAiBG;IACH,6BAA6B,CAC3B,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,SAAK,EACV,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,OAAO,EAChB,SAAS,CAAC,EAAE,OAAO,GAClB,YAAY;IAgMf;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAMzB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI;IAyE1C;;OAEG;IACH,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAgDpD;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IA6BvC;;;;;;;OAOG;IACH,YAAY,CACV,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,SAAK,EACV,IAAI,CAAC,EAAE,MAAM,EACb,MAAM,SAAI,GACT,OAAO,EAAE;IAIZ;;;;;;;;;;;OAWG;IACH,2BAA2B,CACzB,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,SAAK,EACV,IAAI,CAAC,EAAE,MAAM,EACb,MAAM,SAAI,GACT,YAAY;IAgKf;;;;;;;;;;OAUG;IACH,OAAO,CAAC,gBAAgB;IAoCxB;;;;;;;;;;OAUG;IAuBH,SAAS,CACP,EAAE,EAAE,MAAM,EAAE,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EAAE,EACb,GAAG,CAAC,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO;IA0DV;;;;;;;;;;;;OAYG;IACH,eAAe,CACb,UAAU,EAAE,oBAAoB,EAAE,EAClC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE,MAAY,GACpB,iBAAiB,EAAE;IAgDtB;;;;;;;;;;OAUG;IACH,WAAW,CACT,EAAE,EAAE,MAAM,EAAE,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EAAE,EACb,GAAG,CAAC,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO;IAwDV;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,UAAQ,EAAE,IAAI,UAAO,GAAG,OAAO;IAqChF;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,UAAO,GAAG,OAAO;IA2C7E;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAsBzB;;OAEG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAY/B;;OAEG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYjC;;OAEG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYhC;;OAEG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYlC;;OAEG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAYlC;;OAEG;IACH;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,mBAAmB;IAwD3B,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAYnE;;;;;OAKG;IACH,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IAe1D;;;;;;;OAOG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,oBAAoB,EAAE;IAe3F;;OAEG;IACH,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IAStD;;OAEG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IAaxD;;OAEG;IACH,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IASxD;;OAEG;IACH,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE;IAS1D;;;;OAIG;IACH,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,EAAE;IAiEzC;;;;OAIG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAsF7E;;OAEG;IACH,aAAa,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE;IA2C1C;;OAEG;IACH,cAAc,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM;IA8B1D;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAyBtD;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IA0BtD;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IA4C1E;;OAEG;IACH,YAAY,IAAI,OAAO,EAAE;IAIzB;;;OAGG;IACH,OAAO,CAAC,aAAa;IA2CrB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAwBzB;;OAEG;IACH,SAAS,IAAI,QAAQ,EAAE;IAiCvB;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO;IA+B3D;;OAEG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE;IA4DxC,OAAO,CAAC,SAAS,CAAyC;IAC1D,OAAO,CAAC,cAAc,CAAK;IAE3B;;OAEG;IACH,aAAa,IAAI,aAAa,EAAE;IAIhC;;OAEG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAI7C;;OAEG;IACH,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,EAAE,CAAC,EAAE,MAAM,EAAE,EACb,EAAE,CAAC,EAAE,MAAM,EAAE,EACb,EAAE,CAAC,EAAE,MAAM,GACV,aAAa;IAOhB;;OAEG;IACH,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAInC;;OAEG;IACH,WAAW,CACT,EAAE,EAAE,MAAM,EACV,SAAS,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5E,OAAO;IAkBV;;OAEG;IACH,WAAW,IAAI,iBAAiB;IA8EhC;;OAEG;IACH,YAAY,IAAI,SAAS;IA4CzB;;;;;;;OAOG;IACH,wBAAwB,IAAI,qBAAqB;IA6DjD;;;;;;;;;OASG;IACH,aAAa,IAAI,UAAU;CA+D5B"}
|
|
@@ -50,8 +50,26 @@ 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
|
|
55
73
|
/**
|
|
56
74
|
* Merge a per-account SearchDiagnostics into an aggregate (all-accounts) one.
|
|
57
75
|
*
|
|
@@ -66,11 +84,12 @@ export function mergeSearchDiagnostics(into, from) {
|
|
|
66
84
|
}
|
|
67
85
|
/**
|
|
68
86
|
* Split a per-account search payload into its message-list portion and parsed
|
|
69
|
-
* diagnostics. The AppleScript appends a trailer of the form
|
|
87
|
+
* diagnostics. The AppleScript appends a trailer of the form (using the
|
|
88
|
+
* control-character separators defined above):
|
|
70
89
|
*
|
|
71
|
-
* <messages
|
|
90
|
+
* <messages>{DIAG_MARKER}timedOut=true{DIAG_FIELD_SEP}skipped=Foo (9000){DIAG_ITEM_SEP}{DIAG_FIELD_SEP}notSearched=Bar{DIAG_ITEM_SEP}
|
|
72
91
|
*
|
|
73
|
-
* `skipped`/`notSearched` are
|
|
92
|
+
* `skipped`/`notSearched` are DIAG_ITEM_SEP-separated mailbox names, each prefixed
|
|
74
93
|
* with the account name on the way out so the aggregate result is unambiguous.
|
|
75
94
|
*
|
|
76
95
|
* Exported (pure, no Mail.app dependency) for unit testing — this is the logic
|
|
@@ -87,13 +106,13 @@ export function splitSearchDiagnostics(output, account) {
|
|
|
87
106
|
notSearchedMailboxes: [],
|
|
88
107
|
};
|
|
89
108
|
if (trailer) {
|
|
90
|
-
const fields = trailer.split(
|
|
109
|
+
const fields = trailer.split(DIAG_FIELD_SEP);
|
|
91
110
|
const getField = (key) => {
|
|
92
111
|
const f = fields.find((x) => x.startsWith(`${key}=`));
|
|
93
112
|
return f ? f.slice(key.length + 1) : "";
|
|
94
113
|
};
|
|
95
114
|
const splitList = (raw) => raw
|
|
96
|
-
.split(
|
|
115
|
+
.split(DIAG_ITEM_SEP)
|
|
97
116
|
.map((s) => s.trim())
|
|
98
117
|
.filter((s) => s.length > 0);
|
|
99
118
|
diagnostics.skippedLargeMailboxes = splitList(getField("skipped")).map((mb) => `${account} / ${mb}`);
|
|
@@ -521,17 +540,17 @@ export class AppleMailManager {
|
|
|
521
540
|
set msgDateStr to ${AS_DATE_TO_STRING}
|
|
522
541
|
set msgRead to read status of msg as string
|
|
523
542
|
set msgFlagged to flagged status of msg as string
|
|
524
|
-
if msgCount > 0 then set outputText to outputText & "
|
|
525
|
-
set outputText to outputText & msgId & "
|
|
543
|
+
if msgCount > 0 then set outputText to outputText & "${RECORD_SEP}"
|
|
544
|
+
set outputText to outputText & msgId & "${FIELD_SEP}" & msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDateStr & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged
|
|
526
545
|
set msgCount to msgCount + 1
|
|
527
546
|
${dateFilter ? "end if" : ""}
|
|
528
547
|
end try
|
|
529
548
|
end repeat
|
|
530
549
|
on error _errMsg number _errNum
|
|
531
550
|
set _timedOut to true
|
|
532
|
-
set _notSearched to "${escapeForAppleScript(targetMailbox)}
|
|
551
|
+
set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
|
|
533
552
|
end try
|
|
534
|
-
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "
|
|
553
|
+
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=${DIAG_FIELD_SEP}notSearched=" & _notSearched
|
|
535
554
|
`;
|
|
536
555
|
}
|
|
537
556
|
else {
|
|
@@ -556,7 +575,7 @@ export class AppleMailManager {
|
|
|
556
575
|
end try
|
|
557
576
|
if ((current date) - _startedAt) > ${SEARCH_ACCOUNT_BUDGET_SECONDS} then
|
|
558
577
|
set _timedOut to true
|
|
559
|
-
set _notSearched to _notSearched & mbName & "
|
|
578
|
+
set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
|
|
560
579
|
else
|
|
561
580
|
set mbCount to 0
|
|
562
581
|
try
|
|
@@ -564,7 +583,7 @@ export class AppleMailManager {
|
|
|
564
583
|
end try
|
|
565
584
|
if (${scanGuard}) then
|
|
566
585
|
set _timedOut to true
|
|
567
|
-
set _skipped to _skipped & mbName & " (" & (mbCount as string) & ")
|
|
586
|
+
set _skipped to _skipped & mbName & " (" & (mbCount as string) & ")${DIAG_ITEM_SEP}"
|
|
568
587
|
else
|
|
569
588
|
try
|
|
570
589
|
set allMessages to messages of mb ${searchCondition}
|
|
@@ -581,8 +600,8 @@ export class AppleMailManager {
|
|
|
581
600
|
set msgDateStr to ${AS_DATE_TO_STRING}
|
|
582
601
|
set msgRead to read status of msg as string
|
|
583
602
|
set msgFlagged to flagged status of msg as string
|
|
584
|
-
if msgCount > 0 then set outputText to outputText & "
|
|
585
|
-
set outputText to outputText & msgId & "
|
|
603
|
+
if msgCount > 0 then set outputText to outputText & "${RECORD_SEP}"
|
|
604
|
+
set outputText to outputText & msgId & "${FIELD_SEP}" & msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDateStr & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & mbName
|
|
586
605
|
set msgCount to msgCount + 1
|
|
587
606
|
${dateFilter ? "end if" : ""}
|
|
588
607
|
end if
|
|
@@ -590,12 +609,12 @@ export class AppleMailManager {
|
|
|
590
609
|
end repeat
|
|
591
610
|
on error _errMsg number _errNum
|
|
592
611
|
set _timedOut to true
|
|
593
|
-
set _notSearched to _notSearched & mbName & "
|
|
612
|
+
set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
|
|
594
613
|
end try
|
|
595
614
|
end if
|
|
596
615
|
end if
|
|
597
616
|
end repeat
|
|
598
|
-
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "
|
|
617
|
+
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=" & _skipped & "${DIAG_FIELD_SEP}notSearched=" & _notSearched
|
|
599
618
|
`;
|
|
600
619
|
}
|
|
601
620
|
const script = buildAccountScopedScript(targetAccount, searchCommand);
|
|
@@ -668,7 +687,7 @@ export class AppleMailManager {
|
|
|
668
687
|
if rawSrc contains "Content-Disposition: attachment" then set hasAtt to "true"
|
|
669
688
|
end try
|
|
670
689
|
end if
|
|
671
|
-
return msgSubject & "
|
|
690
|
+
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
691
|
end if
|
|
673
692
|
end try
|
|
674
693
|
end repeat
|
|
@@ -683,7 +702,7 @@ export class AppleMailManager {
|
|
|
683
702
|
console.error(`Failed to get message ${id}: ${result.error}`);
|
|
684
703
|
return null;
|
|
685
704
|
}
|
|
686
|
-
const parts = result.output.split(
|
|
705
|
+
const parts = result.output.split(FIELD_SEP);
|
|
687
706
|
if (parts.length < 9)
|
|
688
707
|
return null;
|
|
689
708
|
return {
|
|
@@ -719,7 +738,7 @@ export class AppleMailManager {
|
|
|
719
738
|
try
|
|
720
739
|
set htmlContent to source of msg
|
|
721
740
|
end try
|
|
722
|
-
return msgSubject & "
|
|
741
|
+
return msgSubject & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlContent
|
|
723
742
|
end if
|
|
724
743
|
end try
|
|
725
744
|
end repeat
|
|
@@ -734,10 +753,10 @@ export class AppleMailManager {
|
|
|
734
753
|
console.error(`Failed to get message content: ${result.error}`);
|
|
735
754
|
return null;
|
|
736
755
|
}
|
|
737
|
-
const htmlSplit = result.output.split(
|
|
756
|
+
const htmlSplit = result.output.split(HTML_MARKER);
|
|
738
757
|
const contentPart = htmlSplit[0];
|
|
739
758
|
const htmlContent = htmlSplit.length > 1 ? htmlSplit[1] : undefined;
|
|
740
|
-
const parts = contentPart.split(
|
|
759
|
+
const parts = contentPart.split(CONTENT_MARKER);
|
|
741
760
|
if (parts.length < 2)
|
|
742
761
|
return null;
|
|
743
762
|
return {
|
|
@@ -790,107 +809,172 @@ export class AppleMailManager {
|
|
|
790
809
|
* @returns Array of messages
|
|
791
810
|
*/
|
|
792
811
|
listMessages(mailbox, account, limit = 50, from, offset = 0) {
|
|
793
|
-
|
|
812
|
+
return this.listMessagesWithDiagnostics(mailbox, account, limit, from, offset).messages;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* List messages, returning matches plus coverage diagnostics.
|
|
816
|
+
*
|
|
817
|
+
* Like `searchMessages`, the unscoped (all-mailboxes) path used to iterate
|
|
818
|
+
* `messages of mb` over every mailbox with a swallowing per-mailbox `try`,
|
|
819
|
+
* so a large IMAP/Gmail mailbox timed out and the method returned `[]` — a
|
|
820
|
+
* false "No messages found." This applies the same #24 discipline: skip
|
|
821
|
+
* mailboxes above the scan threshold (reported), enforce a per-account
|
|
822
|
+
* wall-clock budget, capture per-mailbox timeouts, and surface all of it as a
|
|
823
|
+
* partial result. (by-id lookups don't need this — `whose id is` is indexed
|
|
824
|
+
* and returns instantly even on a 44k-message mailbox.)
|
|
825
|
+
*/
|
|
826
|
+
listMessagesWithDiagnostics(mailbox, account, limit = 50, from, offset = 0) {
|
|
827
|
+
// If no account specified, list across all accounts and merge diagnostics.
|
|
794
828
|
if (!account) {
|
|
795
829
|
const accounts = this.listAccounts();
|
|
796
830
|
const allMessages = [];
|
|
831
|
+
const diagnostics = {
|
|
832
|
+
partial: false,
|
|
833
|
+
timedOutAccounts: [],
|
|
834
|
+
skippedLargeMailboxes: [],
|
|
835
|
+
notSearchedMailboxes: [],
|
|
836
|
+
};
|
|
797
837
|
for (const acct of accounts) {
|
|
798
838
|
if (allMessages.length >= limit)
|
|
799
839
|
break;
|
|
800
840
|
const remaining = limit - allMessages.length;
|
|
801
|
-
const
|
|
802
|
-
allMessages.push(...
|
|
841
|
+
const res = this.listMessagesWithDiagnostics(mailbox, acct.name, remaining, from, offset);
|
|
842
|
+
allMessages.push(...res.messages);
|
|
843
|
+
mergeSearchDiagnostics(diagnostics, res.diagnostics);
|
|
803
844
|
}
|
|
804
|
-
return allMessages.slice(0, limit);
|
|
845
|
+
return { messages: allMessages.slice(0, limit), diagnostics };
|
|
805
846
|
}
|
|
806
847
|
const targetAccount = this.resolveAccount(account);
|
|
807
848
|
const safeFrom = from ? escapeForAppleScript(from) : "";
|
|
808
849
|
const fromFilter = from ? `whose sender contains "${safeFrom}"` : "";
|
|
850
|
+
const scanThreshold = getMailboxScanThreshold();
|
|
809
851
|
let listCommand;
|
|
810
852
|
if (mailbox) {
|
|
811
|
-
// List from a specific mailbox
|
|
853
|
+
// List from a specific mailbox. Caller-scoped, so no count-guard skip, but
|
|
854
|
+
// wrap the scan so a timeout is reported as partial, not a false empty.
|
|
812
855
|
const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
|
|
813
856
|
listCommand = `
|
|
814
857
|
set outputText to ""
|
|
858
|
+
set _timedOut to false
|
|
859
|
+
set _notSearched to ""
|
|
815
860
|
set theMailbox to mailbox "${escapeForAppleScript(targetMailbox)}"
|
|
816
861
|
set msgCount to 0
|
|
817
862
|
set skipped to 0
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
863
|
+
try
|
|
864
|
+
repeat with msg in messages of theMailbox ${fromFilter}
|
|
865
|
+
if msgCount >= ${limit} then exit repeat
|
|
866
|
+
try
|
|
867
|
+
if skipped < ${offset} then
|
|
868
|
+
set skipped to skipped + 1
|
|
869
|
+
else
|
|
870
|
+
set msgId to id of msg as string
|
|
871
|
+
set msgSubject to subject of msg
|
|
872
|
+
set msgSender to sender of msg
|
|
873
|
+
set d to date received of msg
|
|
874
|
+
set msgDate to ${AS_DATE_TO_STRING}
|
|
875
|
+
set msgRead to read status of msg as string
|
|
876
|
+
set msgFlagged to flagged status of msg as string
|
|
877
|
+
set msgHasAtt to "false"
|
|
878
|
+
try
|
|
879
|
+
if (count of mail attachments of msg) > 0 then set msgHasAtt to "true"
|
|
880
|
+
end try
|
|
881
|
+
if msgCount > 0 then set outputText to outputText & "${RECORD_SEP}"
|
|
882
|
+
set outputText to outputText & msgId & "${FIELD_SEP}" & msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDate & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & msgHasAtt
|
|
883
|
+
set msgCount to msgCount + 1
|
|
884
|
+
end if
|
|
885
|
+
end try
|
|
886
|
+
end repeat
|
|
887
|
+
on error _errMsg number _errNum
|
|
888
|
+
set _timedOut to true
|
|
889
|
+
set _notSearched to "${escapeForAppleScript(targetMailbox)}${DIAG_ITEM_SEP}"
|
|
890
|
+
end try
|
|
891
|
+
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=${DIAG_FIELD_SEP}notSearched=" & _notSearched
|
|
842
892
|
`;
|
|
843
893
|
}
|
|
844
894
|
else {
|
|
845
|
-
// List from ALL mailboxes —
|
|
895
|
+
// List from ALL mailboxes — skip mailboxes over the scan threshold, enforce
|
|
896
|
+
// the per-account budget, capture per-mailbox timeouts; dedup by message ID.
|
|
897
|
+
const scanGuard = scanThreshold > 0 ? `mbCount > ${scanThreshold}` : "false";
|
|
846
898
|
listCommand = `
|
|
847
899
|
set outputText to ""
|
|
848
900
|
set msgCount to 0
|
|
849
901
|
set skipped to 0
|
|
850
902
|
set seenIds to {}
|
|
903
|
+
set _timedOut to false
|
|
904
|
+
set _skipped to ""
|
|
905
|
+
set _notSearched to ""
|
|
906
|
+
set _startedAt to current date
|
|
851
907
|
repeat with mb in mailboxes
|
|
852
908
|
if msgCount >= ${limit} then exit repeat
|
|
909
|
+
set mbName to ""
|
|
853
910
|
try
|
|
854
|
-
|
|
855
|
-
|
|
911
|
+
set mbName to name of mb
|
|
912
|
+
end try
|
|
913
|
+
if ((current date) - _startedAt) > ${SEARCH_ACCOUNT_BUDGET_SECONDS} then
|
|
914
|
+
set _timedOut to true
|
|
915
|
+
set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
|
|
916
|
+
else
|
|
917
|
+
set mbCount to 0
|
|
918
|
+
try
|
|
919
|
+
set mbCount to count of messages of mb
|
|
920
|
+
end try
|
|
921
|
+
if (${scanGuard}) then
|
|
922
|
+
set _timedOut to true
|
|
923
|
+
set _skipped to _skipped & mbName & " (" & (mbCount as string) & ")${DIAG_ITEM_SEP}"
|
|
924
|
+
else
|
|
856
925
|
try
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
926
|
+
repeat with msg in messages of mb ${fromFilter}
|
|
927
|
+
if msgCount >= ${limit} then exit repeat
|
|
928
|
+
try
|
|
929
|
+
set msgId to id of msg as string
|
|
930
|
+
if seenIds does not contain msgId then
|
|
931
|
+
set end of seenIds to msgId
|
|
932
|
+
if skipped < ${offset} then
|
|
933
|
+
set skipped to skipped + 1
|
|
934
|
+
else
|
|
935
|
+
set msgSubject to subject of msg
|
|
936
|
+
set msgSender to sender of msg
|
|
937
|
+
set d to date received of msg
|
|
938
|
+
set msgDate to ${AS_DATE_TO_STRING}
|
|
939
|
+
set msgRead to read status of msg as string
|
|
940
|
+
set msgFlagged to flagged status of msg as string
|
|
941
|
+
set msgHasAtt to "false"
|
|
942
|
+
try
|
|
943
|
+
if (count of mail attachments of msg) > 0 then set msgHasAtt to "true"
|
|
944
|
+
end try
|
|
945
|
+
if msgCount > 0 then set outputText to outputText & "${RECORD_SEP}"
|
|
946
|
+
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
|
|
947
|
+
set msgCount to msgCount + 1
|
|
948
|
+
end if
|
|
949
|
+
end if
|
|
950
|
+
end try
|
|
951
|
+
end repeat
|
|
952
|
+
on error _errMsg number _errNum
|
|
953
|
+
set _timedOut to true
|
|
954
|
+
set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
|
|
878
955
|
end try
|
|
879
|
-
end
|
|
880
|
-
end
|
|
956
|
+
end if
|
|
957
|
+
end if
|
|
881
958
|
end repeat
|
|
882
|
-
return outputText
|
|
959
|
+
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=" & _skipped & "${DIAG_FIELD_SEP}notSearched=" & _notSearched
|
|
883
960
|
`;
|
|
884
961
|
}
|
|
885
962
|
const script = buildAccountScopedScript(targetAccount, listCommand);
|
|
886
|
-
const result = executeAppleScript(script, { timeoutMs:
|
|
963
|
+
const result = executeAppleScript(script, { timeoutMs: SEARCH_ACCOUNT_TIMEOUT_MS });
|
|
887
964
|
if (!result.success) {
|
|
888
|
-
|
|
889
|
-
|
|
965
|
+
// Whole-account failure — surface as a timeout, not a false empty (#24/#29).
|
|
966
|
+
console.error(`Failed to list messages in "${targetAccount}": ${result.error}`);
|
|
967
|
+
return {
|
|
968
|
+
messages: [],
|
|
969
|
+
diagnostics: {
|
|
970
|
+
partial: true,
|
|
971
|
+
timedOutAccounts: [targetAccount],
|
|
972
|
+
skippedLargeMailboxes: [],
|
|
973
|
+
notSearchedMailboxes: [],
|
|
974
|
+
},
|
|
975
|
+
};
|
|
890
976
|
}
|
|
891
|
-
|
|
892
|
-
return [];
|
|
893
|
-
return this.parseMessageList(result.output, mailbox || "INBOX", targetAccount);
|
|
977
|
+
return this.parseSearchResult(result.output, mailbox || "INBOX", targetAccount);
|
|
894
978
|
}
|
|
895
979
|
/**
|
|
896
980
|
* Parse message list output from AppleScript.
|
|
@@ -904,10 +988,10 @@ export class AppleMailManager {
|
|
|
904
988
|
* limitation). Use getMessage or list-attachments for authoritative info.
|
|
905
989
|
*/
|
|
906
990
|
parseMessageList(output, mailbox, account) {
|
|
907
|
-
const items = output.split(
|
|
991
|
+
const items = output.split(RECORD_SEP);
|
|
908
992
|
const messages = [];
|
|
909
993
|
for (const item of items) {
|
|
910
|
-
const parts = item.split(
|
|
994
|
+
const parts = item.split(FIELD_SEP);
|
|
911
995
|
if (parts.length < 6)
|
|
912
996
|
continue;
|
|
913
997
|
let msgMailbox = mailbox;
|
|
@@ -1488,8 +1572,8 @@ export class AppleMailManager {
|
|
|
1488
1572
|
set attName to name of att
|
|
1489
1573
|
set attType to MIME type of att
|
|
1490
1574
|
set attSize to file size of att as string
|
|
1491
|
-
if attCount > 0 then set outputText to outputText & "
|
|
1492
|
-
set outputText to outputText & attName & "
|
|
1575
|
+
if attCount > 0 then set outputText to outputText & "${RECORD_SEP}"
|
|
1576
|
+
set outputText to outputText & attName & "${FIELD_SEP}" & attType & "${FIELD_SEP}" & attSize
|
|
1493
1577
|
set attCount to attCount + 1
|
|
1494
1578
|
end repeat
|
|
1495
1579
|
return outputText
|
|
@@ -1504,10 +1588,10 @@ export class AppleMailManager {
|
|
|
1504
1588
|
`);
|
|
1505
1589
|
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
1506
1590
|
if (result.success && result.output.trim()) {
|
|
1507
|
-
const items = result.output.split(
|
|
1591
|
+
const items = result.output.split(RECORD_SEP);
|
|
1508
1592
|
const attachments = [];
|
|
1509
1593
|
for (const item of items) {
|
|
1510
|
-
const parts = item.split(
|
|
1594
|
+
const parts = item.split(FIELD_SEP);
|
|
1511
1595
|
if (parts.length < 3)
|
|
1512
1596
|
continue;
|
|
1513
1597
|
attachments.push({
|
|
@@ -1625,9 +1709,9 @@ export class AppleMailManager {
|
|
|
1625
1709
|
set mbName to name of mb
|
|
1626
1710
|
set mbUnread to unread count of mb
|
|
1627
1711
|
set mbCount to count of messages of mb
|
|
1628
|
-
set end of mailboxList to mbName & "
|
|
1712
|
+
set end of mailboxList to mbName & "${FIELD_SEP}" & mbUnread & "${FIELD_SEP}" & mbCount
|
|
1629
1713
|
end repeat
|
|
1630
|
-
set AppleScript's text item delimiters to "
|
|
1714
|
+
set AppleScript's text item delimiters to "${RECORD_SEP}"
|
|
1631
1715
|
return mailboxList as text
|
|
1632
1716
|
`;
|
|
1633
1717
|
const script = buildAccountScopedScript(targetAccount, listCommand);
|
|
@@ -1638,10 +1722,10 @@ export class AppleMailManager {
|
|
|
1638
1722
|
}
|
|
1639
1723
|
if (!result.output.trim())
|
|
1640
1724
|
return [];
|
|
1641
|
-
const items = result.output.split(
|
|
1725
|
+
const items = result.output.split(RECORD_SEP);
|
|
1642
1726
|
const mailboxes = [];
|
|
1643
1727
|
for (const item of items) {
|
|
1644
|
-
const parts = item.split(
|
|
1728
|
+
const parts = item.split(FIELD_SEP);
|
|
1645
1729
|
if (parts.length < 3)
|
|
1646
1730
|
continue;
|
|
1647
1731
|
mailboxes.push({
|
|
@@ -1789,9 +1873,9 @@ export class AppleMailManager {
|
|
|
1789
1873
|
if (count of acctEmail) > 0 then
|
|
1790
1874
|
set emailStr to item 1 of acctEmail
|
|
1791
1875
|
end if
|
|
1792
|
-
set end of accountList to acctName & "
|
|
1876
|
+
set end of accountList to acctName & "${FIELD_SEP}" & emailStr & "${FIELD_SEP}" & acctEnabled
|
|
1793
1877
|
end repeat
|
|
1794
|
-
set AppleScript's text item delimiters to "
|
|
1878
|
+
set AppleScript's text item delimiters to "${RECORD_SEP}"
|
|
1795
1879
|
return accountList as text
|
|
1796
1880
|
`);
|
|
1797
1881
|
const result = executeAppleScript(script);
|
|
@@ -1801,10 +1885,10 @@ export class AppleMailManager {
|
|
|
1801
1885
|
}
|
|
1802
1886
|
if (!result.output.trim())
|
|
1803
1887
|
return [];
|
|
1804
|
-
const items = result.output.split(
|
|
1888
|
+
const items = result.output.split(RECORD_SEP);
|
|
1805
1889
|
const accounts = [];
|
|
1806
1890
|
for (const item of items) {
|
|
1807
|
-
const parts = item.split(
|
|
1891
|
+
const parts = item.split(FIELD_SEP);
|
|
1808
1892
|
if (parts.length < 3)
|
|
1809
1893
|
continue;
|
|
1810
1894
|
accounts.push({
|
|
@@ -1845,19 +1929,19 @@ export class AppleMailManager {
|
|
|
1845
1929
|
repeat with r in rules
|
|
1846
1930
|
set ruleName to name of r
|
|
1847
1931
|
set ruleEnabled to enabled of r
|
|
1848
|
-
set end of ruleList to ruleName & "
|
|
1932
|
+
set end of ruleList to ruleName & "${FIELD_SEP}" & (ruleEnabled as string)
|
|
1849
1933
|
end repeat
|
|
1850
|
-
set AppleScript's text item delimiters to "
|
|
1934
|
+
set AppleScript's text item delimiters to "${RECORD_SEP}"
|
|
1851
1935
|
return ruleList as text
|
|
1852
1936
|
`);
|
|
1853
1937
|
const result = executeAppleScript(script);
|
|
1854
1938
|
if (!result.success || !result.output.trim()) {
|
|
1855
1939
|
return [];
|
|
1856
1940
|
}
|
|
1857
|
-
const items = result.output.split(
|
|
1941
|
+
const items = result.output.split(RECORD_SEP);
|
|
1858
1942
|
const rules = [];
|
|
1859
1943
|
for (const item of items) {
|
|
1860
|
-
const parts = item.split(
|
|
1944
|
+
const parts = item.split(FIELD_SEP);
|
|
1861
1945
|
if (parts.length < 2)
|
|
1862
1946
|
continue;
|
|
1863
1947
|
rules.push({
|
|
@@ -1922,11 +2006,11 @@ export class AppleMailManager {
|
|
|
1922
2006
|
if pPhones is not "" then set pPhones to pPhones & ","
|
|
1923
2007
|
set pPhones to pPhones & (value of ph)
|
|
1924
2008
|
end repeat
|
|
1925
|
-
set end of matchedContacts to pName & "
|
|
2009
|
+
set end of matchedContacts to pName & "${FIELD_SEP}" & pEmails & "${FIELD_SEP}" & pPhones
|
|
1926
2010
|
end if
|
|
1927
2011
|
end repeat
|
|
1928
2012
|
|
|
1929
|
-
set AppleScript's text item delimiters to "
|
|
2013
|
+
set AppleScript's text item delimiters to "${RECORD_SEP}"
|
|
1930
2014
|
return matchedContacts as text
|
|
1931
2015
|
end tell
|
|
1932
2016
|
`;
|
|
@@ -1934,10 +2018,10 @@ export class AppleMailManager {
|
|
|
1934
2018
|
if (!result.success || !result.output.trim()) {
|
|
1935
2019
|
return [];
|
|
1936
2020
|
}
|
|
1937
|
-
const items = result.output.split(
|
|
2021
|
+
const items = result.output.split(RECORD_SEP);
|
|
1938
2022
|
const contacts = [];
|
|
1939
2023
|
for (const item of items) {
|
|
1940
|
-
const parts = item.split(
|
|
2024
|
+
const parts = item.split(FIELD_SEP);
|
|
1941
2025
|
if (parts.length < 3)
|
|
1942
2026
|
continue;
|
|
1943
2027
|
contacts.push({
|
|
@@ -2160,14 +2244,14 @@ export class AppleMailManager {
|
|
|
2160
2244
|
end try
|
|
2161
2245
|
end repeat
|
|
2162
2246
|
|
|
2163
|
-
return (last24h as string) & "
|
|
2247
|
+
return (last24h as string) & "${FIELD_SEP}" & (last7d as string) & "${FIELD_SEP}" & (last30d as string)
|
|
2164
2248
|
`);
|
|
2165
2249
|
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
2166
2250
|
if (!result.success || !result.output.trim()) {
|
|
2167
2251
|
console.error(`Failed to get recently received stats: ${result.error}`);
|
|
2168
2252
|
return { last24h: 0, last7d: 0, last30d: 0 };
|
|
2169
2253
|
}
|
|
2170
|
-
const parts = result.output.split(
|
|
2254
|
+
const parts = result.output.split(FIELD_SEP);
|
|
2171
2255
|
if (parts.length < 3) {
|
|
2172
2256
|
return { last24h: 0, last7d: 0, last30d: 0 };
|
|
2173
2257
|
}
|
|
@@ -2211,7 +2295,7 @@ export class AppleMailManager {
|
|
|
2211
2295
|
set totalMailboxes to totalMailboxes + (count of mailboxes of acct)
|
|
2212
2296
|
end repeat
|
|
2213
2297
|
|
|
2214
|
-
return "running
|
|
2298
|
+
return "running${FIELD_SEP}" & accountCount & "${FIELD_SEP}" & totalMailboxes
|
|
2215
2299
|
`);
|
|
2216
2300
|
const result = executeAppleScript(script);
|
|
2217
2301
|
if (!result.success) {
|
|
@@ -2233,7 +2317,7 @@ export class AppleMailManager {
|
|
|
2233
2317
|
};
|
|
2234
2318
|
}
|
|
2235
2319
|
// Parse the response
|
|
2236
|
-
const parts = result.output.split(
|
|
2320
|
+
const parts = result.output.split(FIELD_SEP);
|
|
2237
2321
|
const isRunning = parts[0] === "running";
|
|
2238
2322
|
const accountCount = parseInt(parts[1]) || 0;
|
|
2239
2323
|
// Mail.app is running with accounts configured - assume sync is active
|