apple-mail-mcp 1.6.8 → 1.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -800,6 +800,7 @@ The entrypoint is written as:
|
|
|
800
800
|
| Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
|
|
801
801
|
| No smart mailboxes | Cannot access Smart Mailboxes via AppleScript |
|
|
802
802
|
| Very large mailboxes not searchable | Apple Mail's AppleScript bridge times out on mailboxes with tens of thousands of messages, so unscoped `search-messages` skips mailboxes above `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default 5000) and reports them as a partial result. Scope with `mailbox` + a date window to search inside one. ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)) |
|
|
803
|
+
| Can't delete/rename server-side mailboxes or mutate drafts | Mail.app's AppleScript bridge can only `delete`/`rename` **local "On My Mac"** mailboxes and cannot delete/move drafts — it throws `AppleEvent handler failed` for IMAP/Gmail/Workspace/iCloud/Exchange mailboxes and drafts (the GUI can do it). `delete-mailbox`/`rename-mailbox`/`delete-message`/`move-message` now return a clear "do it in Mail.app directly" error in that case instead of a generic failure. ([#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) |
|
|
803
804
|
| In-memory templates | Email templates are not persisted across server restarts |
|
|
804
805
|
| Numeric-only message IDs | Message IDs must contain only digits (validated by schema) |
|
|
805
806
|
| Batch size cap | Batch operations are limited to 100 messages per request |
|
package/build/index.js
CHANGED
|
@@ -383,9 +383,9 @@ server.tool("unflag-message", {
|
|
|
383
383
|
server.tool("delete-message", {
|
|
384
384
|
id: MESSAGE_ID_SCHEMA,
|
|
385
385
|
}, withErrorHandling(({ id }) => {
|
|
386
|
-
const success = mailManager.deleteMessage(id);
|
|
386
|
+
const { success, error } = mailManager.deleteMessage(id);
|
|
387
387
|
if (!success) {
|
|
388
|
-
return errorResponse(`Failed to delete message "${id}"`);
|
|
388
|
+
return errorResponse(error || `Failed to delete message "${id}"`);
|
|
389
389
|
}
|
|
390
390
|
return successResponse("Message deleted");
|
|
391
391
|
}, "Error deleting message"));
|
|
@@ -395,9 +395,9 @@ server.tool("move-message", {
|
|
|
395
395
|
mailbox: z.string().min(1, "Destination mailbox is required"),
|
|
396
396
|
account: z.string().optional().describe("Account containing the destination mailbox"),
|
|
397
397
|
}, withErrorHandling(({ id, mailbox, account }) => {
|
|
398
|
-
const success = mailManager.moveMessage(id, mailbox, account);
|
|
398
|
+
const { success, error } = mailManager.moveMessage(id, mailbox, account);
|
|
399
399
|
if (!success) {
|
|
400
|
-
return errorResponse(`Failed to move message to "${mailbox}"`);
|
|
400
|
+
return errorResponse(error || `Failed to move message to "${mailbox}"`);
|
|
401
401
|
}
|
|
402
402
|
return successResponse(`Message moved to "${mailbox}"`);
|
|
403
403
|
}, "Error moving message"));
|
|
@@ -572,9 +572,9 @@ server.tool("delete-mailbox", {
|
|
|
572
572
|
name: z.string().min(1, "Mailbox name is required"),
|
|
573
573
|
account: z.string().optional().describe("Account containing the mailbox"),
|
|
574
574
|
}, withErrorHandling(({ name, account }) => {
|
|
575
|
-
const success = mailManager.deleteMailbox(name, account);
|
|
575
|
+
const { success, error } = mailManager.deleteMailbox(name, account);
|
|
576
576
|
if (!success) {
|
|
577
|
-
return errorResponse(`Failed to delete mailbox "${name}"`);
|
|
577
|
+
return errorResponse(error || `Failed to delete mailbox "${name}"`);
|
|
578
578
|
}
|
|
579
579
|
return successResponse(`Mailbox "${name}" deleted`);
|
|
580
580
|
}, "Error deleting mailbox"));
|
|
@@ -584,9 +584,9 @@ server.tool("rename-mailbox", {
|
|
|
584
584
|
newName: z.string().min(1, "New mailbox name is required"),
|
|
585
585
|
account: z.string().optional().describe("Account containing the mailbox"),
|
|
586
586
|
}, withErrorHandling(({ oldName, newName, account }) => {
|
|
587
|
-
const success = mailManager.renameMailbox(oldName, newName, account);
|
|
587
|
+
const { success, error } = mailManager.renameMailbox(oldName, newName, account);
|
|
588
588
|
if (!success) {
|
|
589
|
-
return errorResponse(`Failed to rename mailbox "${oldName}" to "${newName}"`);
|
|
589
|
+
return errorResponse(error || `Failed to rename mailbox "${oldName}" to "${newName}"`);
|
|
590
590
|
}
|
|
591
591
|
return successResponse(`Mailbox renamed from "${oldName}" to "${newName}"`);
|
|
592
592
|
}, "Error renaming mailbox"));
|
|
@@ -36,6 +36,25 @@ export declare function splitSearchDiagnostics(output: string, account: string):
|
|
|
36
36
|
payload: string;
|
|
37
37
|
diagnostics: SearchDiagnostics;
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* True if `resolvedPath` is one of the allowed roots or strictly inside one.
|
|
41
|
+
*
|
|
42
|
+
* Uses a path-segment boundary check rather than a bare `startsWith`, which
|
|
43
|
+
* would let a sibling whose name merely shares the prefix slip through —
|
|
44
|
+
* `/Volumes-evil` startsWith `/Volumes`, `/Users/robother` startsWith
|
|
45
|
+
* `/Users/rob` (audit finding #12). `resolvedPath` must already be absolute
|
|
46
|
+
* (caller passes `resolve(...)` output).
|
|
47
|
+
*/
|
|
48
|
+
export declare function isPathWithinAllowedRoots(resolvedPath: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Turn a raw mailbox delete/rename failure into an actionable, non-retryable
|
|
51
|
+
* message when it's the known server-side-mailbox limitation (#42); otherwise
|
|
52
|
+
* return the raw error unchanged.
|
|
53
|
+
*
|
|
54
|
+
* Exported for unit testing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function describeMailboxOpError(op: "delete" | "rename", raw: string): string;
|
|
57
|
+
export declare function escapeForAppleScript(text: string): string;
|
|
39
58
|
/**
|
|
40
59
|
* Emits AppleScript that builds a date into the variable `varName` from numeric
|
|
41
60
|
* components.
|
|
@@ -317,7 +336,21 @@ export declare class AppleMailManager {
|
|
|
317
336
|
/**
|
|
318
337
|
* Delete a message.
|
|
319
338
|
*/
|
|
320
|
-
deleteMessage(id: string):
|
|
339
|
+
deleteMessage(id: string): {
|
|
340
|
+
success: boolean;
|
|
341
|
+
error?: string;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Classify a failed message mutation (delete/move) into an actionable error.
|
|
345
|
+
*
|
|
346
|
+
* Mail.app's scripting bridge cannot delete or move drafts, and cannot mutate
|
|
347
|
+
* messages in some server-side special mailboxes — it throws `AppleEvent
|
|
348
|
+
* handler failed` rather than a useful message (#42). When that pattern is
|
|
349
|
+
* seen, look up the message's mailbox (cheap, indexed `whose id is`) to give a
|
|
350
|
+
* draft-specific or server-specific hint. Other errors (e.g. "Message not
|
|
351
|
+
* found", "ambiguous destination") pass through unchanged.
|
|
352
|
+
*/
|
|
353
|
+
private classifyMessageMutationError;
|
|
321
354
|
/**
|
|
322
355
|
* Move a message to a different mailbox.
|
|
323
356
|
*/
|
|
@@ -340,7 +373,10 @@ export declare class AppleMailManager {
|
|
|
340
373
|
* (destination not found / ambiguous / message not found).
|
|
341
374
|
*/
|
|
342
375
|
private moveMessageInternal;
|
|
343
|
-
moveMessage(id: string, mailbox: string, account?: string):
|
|
376
|
+
moveMessage(id: string, mailbox: string, account?: string): {
|
|
377
|
+
success: boolean;
|
|
378
|
+
error?: string;
|
|
379
|
+
};
|
|
344
380
|
/**
|
|
345
381
|
* Run one operation over many message IDs in a SINGLE osascript invocation.
|
|
346
382
|
*
|
|
@@ -413,11 +449,17 @@ export declare class AppleMailManager {
|
|
|
413
449
|
/**
|
|
414
450
|
* Delete a mailbox.
|
|
415
451
|
*/
|
|
416
|
-
deleteMailbox(name: string, account?: string):
|
|
452
|
+
deleteMailbox(name: string, account?: string): {
|
|
453
|
+
success: boolean;
|
|
454
|
+
error?: string;
|
|
455
|
+
};
|
|
417
456
|
/**
|
|
418
457
|
* Rename a mailbox by creating a new one, moving messages, and deleting the old one.
|
|
419
458
|
*/
|
|
420
|
-
renameMailbox(oldName: string, newName: string, account?: string):
|
|
459
|
+
renameMailbox(oldName: string, newName: string, account?: string): {
|
|
460
|
+
success: boolean;
|
|
461
|
+
error?: string;
|
|
462
|
+
};
|
|
421
463
|
/**
|
|
422
464
|
* List all mail accounts (uses cache).
|
|
423
465
|
*/
|
|
@@ -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;AA2DpB;;;;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;
|
|
1
|
+
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { spawnSync } from "child_process";
|
|
16
16
|
import { existsSync, writeFileSync } from "fs";
|
|
17
|
-
import { isAbsolute, resolve } from "path";
|
|
17
|
+
import { isAbsolute, resolve, sep } from "path";
|
|
18
18
|
import { homedir } from "os";
|
|
19
19
|
import { executeAppleScript } from "../utils/applescript.js";
|
|
20
20
|
import { parseMimeAttachments, extractMimeAttachment, extractHtmlBody } from "../utils/mimeParse.js";
|
|
@@ -139,10 +139,61 @@ export function splitSearchDiagnostics(output, account) {
|
|
|
139
139
|
* @param text - Raw text to escape
|
|
140
140
|
* @returns Text safe for AppleScript string embedding
|
|
141
141
|
*/
|
|
142
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Roots under which `save-attachment` is permitted to write.
|
|
144
|
+
*/
|
|
145
|
+
const ALLOWED_SAVE_ROOTS = [homedir(), "/tmp", "/private/tmp", "/Volumes"];
|
|
146
|
+
/**
|
|
147
|
+
* True if `resolvedPath` is one of the allowed roots or strictly inside one.
|
|
148
|
+
*
|
|
149
|
+
* Uses a path-segment boundary check rather than a bare `startsWith`, which
|
|
150
|
+
* would let a sibling whose name merely shares the prefix slip through —
|
|
151
|
+
* `/Volumes-evil` startsWith `/Volumes`, `/Users/robother` startsWith
|
|
152
|
+
* `/Users/rob` (audit finding #12). `resolvedPath` must already be absolute
|
|
153
|
+
* (caller passes `resolve(...)` output).
|
|
154
|
+
*/
|
|
155
|
+
export function isPathWithinAllowedRoots(resolvedPath) {
|
|
156
|
+
return ALLOWED_SAVE_ROOTS.some((root) => {
|
|
157
|
+
const base = root.endsWith(sep) ? root.slice(0, -1) : root;
|
|
158
|
+
return resolvedPath === base || resolvedPath.startsWith(base + sep);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Pattern in a raw AppleScript error that indicates an operation Mail.app's
|
|
163
|
+
* scripting interface simply cannot perform on this target — most often a
|
|
164
|
+
* server-side (IMAP / Gmail / Workspace / iCloud / Exchange) mailbox or a draft.
|
|
165
|
+
* Mail throws `AppleEvent handler failed` (-10000) for these; the GUI can do
|
|
166
|
+
* them, the scripting bridge cannot. See issue #42 and the audit doc.
|
|
167
|
+
*/
|
|
168
|
+
const UNSUPPORTED_APPLESCRIPT_OP = /AppleEvent handler failed|-10000/i;
|
|
169
|
+
/**
|
|
170
|
+
* Turn a raw mailbox delete/rename failure into an actionable, non-retryable
|
|
171
|
+
* message when it's the known server-side-mailbox limitation (#42); otherwise
|
|
172
|
+
* return the raw error unchanged.
|
|
173
|
+
*
|
|
174
|
+
* Exported for unit testing.
|
|
175
|
+
*/
|
|
176
|
+
export function describeMailboxOpError(op, raw) {
|
|
177
|
+
const trimmed = (raw || "").trim();
|
|
178
|
+
if (UNSUPPORTED_APPLESCRIPT_OP.test(trimmed)) {
|
|
179
|
+
const verb = op === "delete" ? "Delete" : "Rename";
|
|
180
|
+
return `Mail.app cannot ${op} server-side (IMAP / Gmail / Workspace / iCloud / Exchange) mailboxes via AppleScript — only local "On My Mac" mailboxes support this. ${verb} it in Mail.app directly. (Mail.app error: ${trimmed})`;
|
|
181
|
+
}
|
|
182
|
+
return trimmed || `Failed to ${op} mailbox`;
|
|
183
|
+
}
|
|
184
|
+
export function escapeForAppleScript(text) {
|
|
143
185
|
if (!text)
|
|
144
186
|
return "";
|
|
145
|
-
|
|
187
|
+
// Escape backslash and double-quote for the AppleScript string literal, and
|
|
188
|
+
// strip ASCII control characters. An AppleScript double-quoted literal cannot
|
|
189
|
+
// contain a raw newline, so an interpolated value with a `\n` (or other
|
|
190
|
+
// control char) would terminate the literal early and could inject a
|
|
191
|
+
// statement; stripping them closes that gap (audit finding #10).
|
|
192
|
+
return (text
|
|
193
|
+
.replace(/\\/g, "\\\\")
|
|
194
|
+
.replace(/"/g, '\\"')
|
|
195
|
+
// eslint-disable-next-line no-control-regex
|
|
196
|
+
.replace(/[\x00-\x1f\x7f]/g, ""));
|
|
146
197
|
}
|
|
147
198
|
/**
|
|
148
199
|
* Validates attachment file paths and builds AppleScript commands to attach them.
|
|
@@ -1397,11 +1448,41 @@ export class AppleMailManager {
|
|
|
1397
1448
|
deleteMessage(id) {
|
|
1398
1449
|
const script = this.findMessageScript(id, "delete msg");
|
|
1399
1450
|
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1402
|
-
return false;
|
|
1451
|
+
if (result.success && !result.output.startsWith("error:")) {
|
|
1452
|
+
return { success: true };
|
|
1403
1453
|
}
|
|
1404
|
-
|
|
1454
|
+
const raw = result.success
|
|
1455
|
+
? result.output.replace(/^error:/, "")
|
|
1456
|
+
: result.error || "Unknown error";
|
|
1457
|
+
const error = this.classifyMessageMutationError(id, raw, "delete");
|
|
1458
|
+
console.error(`Failed to delete message: ${error}`);
|
|
1459
|
+
return { success: false, error };
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Classify a failed message mutation (delete/move) into an actionable error.
|
|
1463
|
+
*
|
|
1464
|
+
* Mail.app's scripting bridge cannot delete or move drafts, and cannot mutate
|
|
1465
|
+
* messages in some server-side special mailboxes — it throws `AppleEvent
|
|
1466
|
+
* handler failed` rather than a useful message (#42). When that pattern is
|
|
1467
|
+
* seen, look up the message's mailbox (cheap, indexed `whose id is`) to give a
|
|
1468
|
+
* draft-specific or server-specific hint. Other errors (e.g. "Message not
|
|
1469
|
+
* found", "ambiguous destination") pass through unchanged.
|
|
1470
|
+
*/
|
|
1471
|
+
classifyMessageMutationError(id, raw, op) {
|
|
1472
|
+
const trimmed = (raw || "").trim();
|
|
1473
|
+
if (!UNSUPPORTED_APPLESCRIPT_OP.test(trimmed))
|
|
1474
|
+
return trimmed || `Failed to ${op} message`;
|
|
1475
|
+
let mailbox = "";
|
|
1476
|
+
try {
|
|
1477
|
+
mailbox = this.getMessageById(id)?.mailbox ?? "";
|
|
1478
|
+
}
|
|
1479
|
+
catch {
|
|
1480
|
+
/* best-effort; fall through to the generic server-side message */
|
|
1481
|
+
}
|
|
1482
|
+
if (/draft/i.test(mailbox)) {
|
|
1483
|
+
return `Mail.app cannot ${op} drafts via AppleScript; ${op} it in Mail.app directly. (Mail.app error: ${trimmed})`;
|
|
1484
|
+
}
|
|
1485
|
+
return `Mail.app cannot ${op} this message via AppleScript (server-side or special mailbox${mailbox ? ` "${mailbox}"` : ""}); ${op} it in Mail.app directly. (Mail.app error: ${trimmed})`;
|
|
1405
1486
|
}
|
|
1406
1487
|
/**
|
|
1407
1488
|
* Move a message to a different mailbox.
|
|
@@ -1473,11 +1554,12 @@ export class AppleMailManager {
|
|
|
1473
1554
|
return { success: true };
|
|
1474
1555
|
}
|
|
1475
1556
|
moveMessage(id, mailbox, account) {
|
|
1476
|
-
const
|
|
1477
|
-
if (
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1557
|
+
const res = this.moveMessageInternal(id, mailbox, account);
|
|
1558
|
+
if (res.success)
|
|
1559
|
+
return { success: true };
|
|
1560
|
+
const error = this.classifyMessageMutationError(id, res.error || "Failed to move message", "move");
|
|
1561
|
+
console.error(`Failed to move message: ${error}`);
|
|
1562
|
+
return { success: false, error };
|
|
1481
1563
|
}
|
|
1482
1564
|
// ===========================================================================
|
|
1483
1565
|
// Batch Operations
|
|
@@ -1730,9 +1812,7 @@ export class AppleMailManager {
|
|
|
1730
1812
|
}
|
|
1731
1813
|
// Resolve the save path to prevent symlink / ".." traversal bypass
|
|
1732
1814
|
const resolvedPath = resolve(savePath);
|
|
1733
|
-
|
|
1734
|
-
const isAllowed = allowedPrefixes.some((prefix) => resolvedPath.startsWith(prefix));
|
|
1735
|
-
if (!isAllowed) {
|
|
1815
|
+
if (!isPathWithinAllowedRoots(resolvedPath)) {
|
|
1736
1816
|
console.error(`Save path "${savePath}" is outside allowed directories`);
|
|
1737
1817
|
return false;
|
|
1738
1818
|
}
|
|
@@ -1783,8 +1863,7 @@ export class AppleMailManager {
|
|
|
1783
1863
|
try {
|
|
1784
1864
|
const outPath = resolve(resolvedPath, attachmentName);
|
|
1785
1865
|
// Verify the resolved output path is still within allowed directories
|
|
1786
|
-
|
|
1787
|
-
if (!isOutAllowed) {
|
|
1866
|
+
if (!isPathWithinAllowedRoots(outPath)) {
|
|
1788
1867
|
console.error(`Output path "${outPath}" is outside allowed directories`);
|
|
1789
1868
|
return false;
|
|
1790
1869
|
}
|
|
@@ -1816,7 +1895,10 @@ export class AppleMailManager {
|
|
|
1816
1895
|
return mailboxList as text
|
|
1817
1896
|
`;
|
|
1818
1897
|
const script = buildAccountScopedScript(targetAccount, listCommand);
|
|
1819
|
-
|
|
1898
|
+
// Counts every mailbox's message total, so it needs more than the default
|
|
1899
|
+
// 30s on accounts with many/large mailboxes; a timeout here silently
|
|
1900
|
+
// returned an empty list (audit finding #8).
|
|
1901
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
1820
1902
|
if (!result.success) {
|
|
1821
1903
|
console.error(`Failed to list mailboxes: ${result.error}`);
|
|
1822
1904
|
return [];
|
|
@@ -1860,7 +1942,9 @@ export class AppleMailManager {
|
|
|
1860
1942
|
`;
|
|
1861
1943
|
}
|
|
1862
1944
|
const script = buildAccountScopedScript(targetAccount, command);
|
|
1863
|
-
|
|
1945
|
+
// Summing unread across every mailbox can exceed the default 30s; a timeout
|
|
1946
|
+
// previously degraded silently to 0 ("all read") — audit finding #8.
|
|
1947
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
1864
1948
|
if (!result.success) {
|
|
1865
1949
|
console.error(`Failed to get unread count: ${result.error}`);
|
|
1866
1950
|
return 0;
|
|
@@ -1908,11 +1992,15 @@ export class AppleMailManager {
|
|
|
1908
1992
|
`);
|
|
1909
1993
|
const result = executeAppleScript(script);
|
|
1910
1994
|
if (!result.success || result.output.startsWith("error:")) {
|
|
1911
|
-
|
|
1912
|
-
|
|
1995
|
+
const raw = result.success
|
|
1996
|
+
? result.output.replace(/^error:/, "")
|
|
1997
|
+
: result.error || "Unknown error";
|
|
1998
|
+
const error = describeMailboxOpError("delete", raw);
|
|
1999
|
+
console.error(`Failed to delete mailbox: ${error}`);
|
|
2000
|
+
return { success: false, error };
|
|
1913
2001
|
}
|
|
1914
2002
|
this.invalidateCache();
|
|
1915
|
-
return true;
|
|
2003
|
+
return { success: true };
|
|
1916
2004
|
}
|
|
1917
2005
|
/**
|
|
1918
2006
|
* Rename a mailbox by creating a new one, moving messages, and deleting the old one.
|
|
@@ -1921,7 +2009,10 @@ export class AppleMailManager {
|
|
|
1921
2009
|
const targetAccount = this.resolveAccount(account);
|
|
1922
2010
|
// Create the new mailbox
|
|
1923
2011
|
if (!this.createMailbox(newName, targetAccount)) {
|
|
1924
|
-
return
|
|
2012
|
+
return {
|
|
2013
|
+
success: false,
|
|
2014
|
+
error: `Could not create the destination mailbox "${newName}" needed for the rename.`,
|
|
2015
|
+
};
|
|
1925
2016
|
}
|
|
1926
2017
|
// Move all messages from old to new
|
|
1927
2018
|
const resolvedOld = this.resolveMailbox(oldName, targetAccount);
|
|
@@ -1967,19 +2058,27 @@ export class AppleMailManager {
|
|
|
1967
2058
|
// check), so a truncated move is recoverable rather than lossy.
|
|
1968
2059
|
const result = executeAppleScript(moveScript, { timeoutMs: 120000 });
|
|
1969
2060
|
if (!result.success || result.output.startsWith("error:")) {
|
|
1970
|
-
|
|
1971
|
-
|
|
2061
|
+
const raw = result.success
|
|
2062
|
+
? result.output.replace(/^error:/, "")
|
|
2063
|
+
: result.error || "Unknown error";
|
|
2064
|
+
// The empty destination mailbox we just created is now an orphan; the
|
|
2065
|
+
// source is untouched (delete only runs after a verified-empty move).
|
|
2066
|
+
const error = describeMailboxOpError("rename", raw);
|
|
2067
|
+
console.error(`Failed to rename mailbox: ${error}`);
|
|
2068
|
+
this.invalidateCache();
|
|
2069
|
+
return { success: false, error };
|
|
1972
2070
|
}
|
|
1973
2071
|
if (result.output.startsWith("partial")) {
|
|
1974
2072
|
const parts = result.output.split(FIELD_SEP);
|
|
1975
2073
|
const remaining = parts[3] ?? "?";
|
|
1976
2074
|
const total = parts[2] ?? "?";
|
|
1977
|
-
|
|
2075
|
+
const error = `Only ${parts[1] ?? "?"} of ${total} messages moved, ${remaining} remain in "${resolvedOld}"; the source was NOT deleted (both mailboxes left intact). Retry to move the rest.`;
|
|
2076
|
+
console.error(`Failed to rename mailbox: ${error}`);
|
|
1978
2077
|
this.invalidateCache(); // the new mailbox now exists and holds the moved messages
|
|
1979
|
-
return false;
|
|
2078
|
+
return { success: false, error };
|
|
1980
2079
|
}
|
|
1981
2080
|
this.invalidateCache();
|
|
1982
|
-
return true;
|
|
2081
|
+
return { success: true };
|
|
1983
2082
|
}
|
|
1984
2083
|
// ===========================================================================
|
|
1985
2084
|
// Account Operations
|
|
@@ -2127,18 +2226,22 @@ export class AppleMailManager {
|
|
|
2127
2226
|
set pid to id of p
|
|
2128
2227
|
if seenIds does not contain pid then
|
|
2129
2228
|
set end of seenIds to pid
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
set pEmails to
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
set pPhones to
|
|
2140
|
-
|
|
2141
|
-
|
|
2229
|
+
-- Per-person try: one malformed contact (e.g. no emails) must not
|
|
2230
|
+
-- abort the whole search and return an empty list (audit finding #13).
|
|
2231
|
+
try
|
|
2232
|
+
set pName to name of p
|
|
2233
|
+
set pEmails to ""
|
|
2234
|
+
repeat with e in emails of p
|
|
2235
|
+
if pEmails is not "" then set pEmails to pEmails & ","
|
|
2236
|
+
set pEmails to pEmails & (value of e)
|
|
2237
|
+
end repeat
|
|
2238
|
+
set pPhones to ""
|
|
2239
|
+
repeat with ph in phones of p
|
|
2240
|
+
if pPhones is not "" then set pPhones to pPhones & ","
|
|
2241
|
+
set pPhones to pPhones & (value of ph)
|
|
2242
|
+
end repeat
|
|
2243
|
+
set end of matchedContacts to pName & "${FIELD_SEP}" & pEmails & "${FIELD_SEP}" & pPhones
|
|
2244
|
+
end try
|
|
2142
2245
|
end if
|
|
2143
2246
|
end repeat
|
|
2144
2247
|
|
|
@@ -2203,10 +2306,13 @@ export class AppleMailManager {
|
|
|
2203
2306
|
const template = this.templates.get(id);
|
|
2204
2307
|
if (!template)
|
|
2205
2308
|
return false;
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
const
|
|
2309
|
+
// Use `??` (not `||`) for subject/body so an intentional empty-string
|
|
2310
|
+
// override is honored rather than falling back to the template value
|
|
2311
|
+
// (audit finding #14).
|
|
2312
|
+
const to = overrides?.to ?? template.to ?? [];
|
|
2313
|
+
const cc = overrides?.cc ?? template.cc;
|
|
2314
|
+
const subject = overrides?.subject ?? template.subject;
|
|
2315
|
+
const body = overrides?.body ?? template.body;
|
|
2210
2316
|
if (to.length === 0)
|
|
2211
2317
|
return false;
|
|
2212
2318
|
return this.createDraft(to, subject, body, cc);
|
|
@@ -2429,7 +2535,9 @@ export class AppleMailManager {
|
|
|
2429
2535
|
|
|
2430
2536
|
return "running${FIELD_SEP}" & accountCount & "${FIELD_SEP}" & totalMailboxes
|
|
2431
2537
|
`);
|
|
2432
|
-
|
|
2538
|
+
// Counts mailboxes across every account; give it headroom over the 30s
|
|
2539
|
+
// default so a slow account doesn't silently report "not syncing" (#8).
|
|
2540
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
2433
2541
|
if (!result.success) {
|
|
2434
2542
|
return {
|
|
2435
2543
|
syncDetected: false,
|