apple-mail-mcp 1.6.5 → 1.6.7
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 +6 -2
- package/build/services/appleMailManager.d.ts +34 -14
- package/build/services/appleMailManager.d.ts.map +1 -1
- package/build/services/appleMailManager.js +182 -81
- package/build/utils/mimeParse.d.ts +15 -0
- package/build/utils/mimeParse.d.ts.map +1 -1
- package/build/utils/mimeParse.js +38 -0
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -174,9 +174,13 @@ server.tool("search-messages", {
|
|
|
174
174
|
// --- get-message ---
|
|
175
175
|
server.tool("get-message", {
|
|
176
176
|
id: MESSAGE_ID_SCHEMA,
|
|
177
|
-
preferHtml: z
|
|
177
|
+
preferHtml: z
|
|
178
|
+
.boolean()
|
|
179
|
+
.optional()
|
|
180
|
+
.describe("Return the HTML body (extracted from the message source) instead of plain text"),
|
|
178
181
|
}, withErrorHandling(({ id, preferHtml }) => {
|
|
179
|
-
|
|
182
|
+
// Only fetch/parse the raw source when HTML is actually requested (#32).
|
|
183
|
+
const content = mailManager.getMessageContent(id, preferHtml === true);
|
|
180
184
|
if (!content) {
|
|
181
185
|
return errorResponse(`Message with ID "${id}" not found`);
|
|
182
186
|
}
|
|
@@ -180,11 +180,18 @@ export declare class AppleMailManager {
|
|
|
180
180
|
* Note: Mail.app message IDs are unique per mailbox. This method searches
|
|
181
181
|
* all mailboxes in all accounts to find the message.
|
|
182
182
|
*/
|
|
183
|
-
getMessageById(id: string): Message | null;
|
|
183
|
+
getMessageById(id: string, deepAttachmentCheck?: boolean): Message | null;
|
|
184
184
|
/**
|
|
185
185
|
* Get the content of a message.
|
|
186
|
+
*
|
|
187
|
+
* @param id - Message ID
|
|
188
|
+
* @param includeHtml - When true, also fetch the raw MIME source and extract
|
|
189
|
+
* the `text/html` body part into `htmlContent`. This is opt-in because the
|
|
190
|
+
* source can be MB-sized (it includes base64 attachments) and the plain-text
|
|
191
|
+
* path doesn't need it; fetching it unconditionally was both slow and, worse,
|
|
192
|
+
* returned the entire raw MIME blob mislabeled as HTML (#32).
|
|
186
193
|
*/
|
|
187
|
-
getMessageContent(id: string): MessageContent | null;
|
|
194
|
+
getMessageContent(id: string, includeHtml?: boolean): MessageContent | null;
|
|
188
195
|
/**
|
|
189
196
|
* Get the raw MIME source of a message.
|
|
190
197
|
* Used as fallback for attachment extraction when AppleScript
|
|
@@ -335,35 +342,48 @@ export declare class AppleMailManager {
|
|
|
335
342
|
private moveMessageInternal;
|
|
336
343
|
moveMessage(id: string, mailbox: string, account?: string): boolean;
|
|
337
344
|
/**
|
|
338
|
-
*
|
|
345
|
+
* Run one operation over many message IDs in a SINGLE osascript invocation.
|
|
346
|
+
*
|
|
347
|
+
* Previously each batch method looped and called the per-id method, so a
|
|
348
|
+
* 100-id batch spawned 100 osascript processes — each one re-resolving
|
|
349
|
+
* accounts and walking the whole account→mailbox tree — all serialized
|
|
350
|
+
* through the gate (issue #31). This walks the tree exactly once: for each
|
|
351
|
+
* mailbox it probes the still-pending IDs with `whose id is` (indexed, so
|
|
352
|
+
* effectively free) and applies `operation` to any match, tracking found IDs
|
|
353
|
+
* so it can stop early once all are accounted for. Per-id outcomes come back
|
|
354
|
+
* as control-char-delimited `id<FS>status` records (status: `ok`,
|
|
355
|
+
* `notfound`, or `error:<msg>`), and results are returned in input order.
|
|
339
356
|
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
357
|
+
* `setup` runs once before the walk (used by move to resolve the destination);
|
|
358
|
+
* it may bail the whole batch by returning a `BATCH_FATAL`-prefixed string.
|
|
359
|
+
*/
|
|
360
|
+
private runBatchOperation;
|
|
361
|
+
/**
|
|
362
|
+
* Delete multiple messages at once (single tree walk — see runBatchOperation).
|
|
342
363
|
*/
|
|
343
364
|
batchDeleteMessages(ids: string[]): BatchOperationResult[];
|
|
344
365
|
/**
|
|
345
|
-
* Move multiple messages to a mailbox at once.
|
|
366
|
+
* Move multiple messages to a mailbox at once (single tree walk).
|
|
346
367
|
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* @returns Array of results for each message
|
|
368
|
+
* The destination is resolved once (account-scoped, ambiguity-aware — a name
|
|
369
|
+
* matching more than one mailbox fails the whole batch rather than guessing),
|
|
370
|
+
* then every matched message is moved in the same walk.
|
|
351
371
|
*/
|
|
352
372
|
batchMoveMessages(ids: string[], mailbox: string, account?: string): BatchOperationResult[];
|
|
353
373
|
/**
|
|
354
|
-
* Mark multiple messages as read at once.
|
|
374
|
+
* Mark multiple messages as read at once (single tree walk).
|
|
355
375
|
*/
|
|
356
376
|
batchMarkAsRead(ids: string[]): BatchOperationResult[];
|
|
357
377
|
/**
|
|
358
|
-
* Mark multiple messages as unread at once.
|
|
378
|
+
* Mark multiple messages as unread at once (single tree walk).
|
|
359
379
|
*/
|
|
360
380
|
batchMarkAsUnread(ids: string[]): BatchOperationResult[];
|
|
361
381
|
/**
|
|
362
|
-
* Flag multiple messages at once.
|
|
382
|
+
* Flag multiple messages at once (single tree walk).
|
|
363
383
|
*/
|
|
364
384
|
batchFlagMessages(ids: string[]): BatchOperationResult[];
|
|
365
385
|
/**
|
|
366
|
-
* Unflag multiple messages at once.
|
|
386
|
+
* Unflag multiple messages at once (single tree walk).
|
|
367
387
|
*/
|
|
368
388
|
batchUnflagMessages(ids: string[]): BatchOperationResult[];
|
|
369
389
|
/**
|
|
@@ -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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"}
|
|
@@ -17,7 +17,7 @@ import { existsSync, writeFileSync } from "fs";
|
|
|
17
17
|
import { isAbsolute, resolve } from "path";
|
|
18
18
|
import { homedir } from "os";
|
|
19
19
|
import { executeAppleScript } from "../utils/applescript.js";
|
|
20
|
-
import { parseMimeAttachments, extractMimeAttachment } from "../utils/mimeParse.js";
|
|
20
|
+
import { parseMimeAttachments, extractMimeAttachment, extractHtmlBody } from "../utils/mimeParse.js";
|
|
21
21
|
// =============================================================================
|
|
22
22
|
// Search Tuning (issue #24)
|
|
23
23
|
// =============================================================================
|
|
@@ -70,6 +70,7 @@ const DIAG_FIELD_SEP = "\x1dF\x1d"; // between diagnostics fields
|
|
|
70
70
|
const DIAG_ITEM_SEP = "\x1dM\x1d"; // between diagnostics list items
|
|
71
71
|
const CONTENT_MARKER = "\x1dCONTENT\x1d"; // subject/plain-text boundary
|
|
72
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)
|
|
73
74
|
/**
|
|
74
75
|
* Merge a per-account SearchDiagnostics into an aggregate (all-accounts) one.
|
|
75
76
|
*
|
|
@@ -651,7 +652,21 @@ export class AppleMailManager {
|
|
|
651
652
|
* Note: Mail.app message IDs are unique per mailbox. This method searches
|
|
652
653
|
* all mailboxes in all accounts to find the message.
|
|
653
654
|
*/
|
|
654
|
-
getMessageById(id) {
|
|
655
|
+
getMessageById(id, deepAttachmentCheck = false) {
|
|
656
|
+
// MIME-embedded attachments are invisible to AppleScript's `mail attachments`
|
|
657
|
+
// object, so the only way to detect them is to scan the raw `source of msg`.
|
|
658
|
+
// That reads the entire message (can be MB-sized), so it's the slowest part
|
|
659
|
+
// of this path — now opt-in via `deepAttachmentCheck` rather than run on
|
|
660
|
+
// every attachmentless message (#32). Default off: hasAttachments reflects
|
|
661
|
+
// the fast attachment count only.
|
|
662
|
+
const deepScan = deepAttachmentCheck
|
|
663
|
+
? `if hasAtt is "false" then
|
|
664
|
+
try
|
|
665
|
+
set rawSrc to source of msg
|
|
666
|
+
if rawSrc contains "Content-Disposition: attachment" then set hasAtt to "true"
|
|
667
|
+
end try
|
|
668
|
+
end if`
|
|
669
|
+
: "";
|
|
655
670
|
const script = buildAppLevelScript(`
|
|
656
671
|
try
|
|
657
672
|
repeat with acct in accounts
|
|
@@ -675,18 +690,7 @@ export class AppleMailManager {
|
|
|
675
690
|
set attCount to count of mail attachments of msg
|
|
676
691
|
if attCount > 0 then set hasAtt to "true"
|
|
677
692
|
end try
|
|
678
|
-
|
|
679
|
-
-- attachment object. Fall back to scanning the raw source.
|
|
680
|
-
-- This reads the full message source (can be MB-sized for
|
|
681
|
-
-- messages with large bodies), so it's the slowest part of
|
|
682
|
-
-- get-message for attachmentless messages. Accepted as the
|
|
683
|
-
-- cost of correct hasAttachments in the detail view.
|
|
684
|
-
if hasAtt is "false" then
|
|
685
|
-
try
|
|
686
|
-
set rawSrc to source of msg
|
|
687
|
-
if rawSrc contains "Content-Disposition: attachment" then set hasAtt to "true"
|
|
688
|
-
end try
|
|
689
|
-
end if
|
|
693
|
+
${deepScan}
|
|
690
694
|
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
|
|
691
695
|
end if
|
|
692
696
|
end try
|
|
@@ -722,8 +726,20 @@ export class AppleMailManager {
|
|
|
722
726
|
}
|
|
723
727
|
/**
|
|
724
728
|
* Get the content of a message.
|
|
729
|
+
*
|
|
730
|
+
* @param id - Message ID
|
|
731
|
+
* @param includeHtml - When true, also fetch the raw MIME source and extract
|
|
732
|
+
* the `text/html` body part into `htmlContent`. This is opt-in because the
|
|
733
|
+
* source can be MB-sized (it includes base64 attachments) and the plain-text
|
|
734
|
+
* path doesn't need it; fetching it unconditionally was both slow and, worse,
|
|
735
|
+
* returned the entire raw MIME blob mislabeled as HTML (#32).
|
|
725
736
|
*/
|
|
726
|
-
getMessageContent(id) {
|
|
737
|
+
getMessageContent(id, includeHtml = false) {
|
|
738
|
+
// Only `source of msg` is fetched when HTML is requested. `content of msg`
|
|
739
|
+
// is the plain-text body and is always cheap.
|
|
740
|
+
const sourceFetch = includeHtml
|
|
741
|
+
? `set htmlSource to ""\n try\n set htmlSource to source of msg\n end try`
|
|
742
|
+
: `set htmlSource to ""`;
|
|
727
743
|
const script = buildAppLevelScript(`
|
|
728
744
|
try
|
|
729
745
|
repeat with acct in accounts
|
|
@@ -734,11 +750,8 @@ export class AppleMailManager {
|
|
|
734
750
|
set msg to item 1 of matchingMsgs
|
|
735
751
|
set msgSubject to subject of msg
|
|
736
752
|
set msgContent to content of msg
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
set htmlContent to source of msg
|
|
740
|
-
end try
|
|
741
|
-
return msgSubject & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlContent
|
|
753
|
+
${sourceFetch}
|
|
754
|
+
return msgSubject & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlSource
|
|
742
755
|
end if
|
|
743
756
|
end try
|
|
744
757
|
end repeat
|
|
@@ -755,15 +768,19 @@ export class AppleMailManager {
|
|
|
755
768
|
}
|
|
756
769
|
const htmlSplit = result.output.split(HTML_MARKER);
|
|
757
770
|
const contentPart = htmlSplit[0];
|
|
758
|
-
const
|
|
771
|
+
const rawSource = htmlSplit.length > 1 ? htmlSplit[1] : "";
|
|
759
772
|
const parts = contentPart.split(CONTENT_MARKER);
|
|
760
773
|
if (parts.length < 2)
|
|
761
774
|
return null;
|
|
775
|
+
// Extract the actual text/html body from the raw MIME source rather than
|
|
776
|
+
// returning the whole source. Falls back to undefined when the message has
|
|
777
|
+
// no HTML part (e.g. a plain-text-only email).
|
|
778
|
+
const htmlContent = includeHtml && rawSource ? extractHtmlBody(rawSource) || undefined : undefined;
|
|
762
779
|
return {
|
|
763
780
|
id: id.toString(),
|
|
764
781
|
subject: parts[0],
|
|
765
782
|
plainText: parts[1],
|
|
766
|
-
htmlContent
|
|
783
|
+
htmlContent,
|
|
767
784
|
};
|
|
768
785
|
}
|
|
769
786
|
/**
|
|
@@ -1466,90 +1483,174 @@ export class AppleMailManager {
|
|
|
1466
1483
|
// Batch Operations
|
|
1467
1484
|
// ===========================================================================
|
|
1468
1485
|
/**
|
|
1469
|
-
*
|
|
1486
|
+
* Run one operation over many message IDs in a SINGLE osascript invocation.
|
|
1487
|
+
*
|
|
1488
|
+
* Previously each batch method looped and called the per-id method, so a
|
|
1489
|
+
* 100-id batch spawned 100 osascript processes — each one re-resolving
|
|
1490
|
+
* accounts and walking the whole account→mailbox tree — all serialized
|
|
1491
|
+
* through the gate (issue #31). This walks the tree exactly once: for each
|
|
1492
|
+
* mailbox it probes the still-pending IDs with `whose id is` (indexed, so
|
|
1493
|
+
* effectively free) and applies `operation` to any match, tracking found IDs
|
|
1494
|
+
* so it can stop early once all are accounted for. Per-id outcomes come back
|
|
1495
|
+
* as control-char-delimited `id<FS>status` records (status: `ok`,
|
|
1496
|
+
* `notfound`, or `error:<msg>`), and results are returned in input order.
|
|
1470
1497
|
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1498
|
+
* `setup` runs once before the walk (used by move to resolve the destination);
|
|
1499
|
+
* it may bail the whole batch by returning a `BATCH_FATAL`-prefixed string.
|
|
1473
1500
|
*/
|
|
1474
|
-
|
|
1475
|
-
|
|
1501
|
+
runBatchOperation(ids, operation, setup = "") {
|
|
1502
|
+
// Keep the numeric IDs paired with their original string form and 1-based
|
|
1503
|
+
// position. The AppleScript reports outcomes by POSITION, not by id: a Mail
|
|
1504
|
+
// id large enough to exceed AppleScript's 2^29 integer range coerces to
|
|
1505
|
+
// scientific notation under `as string` (999999999 -> "9.99999999E+8"), so
|
|
1506
|
+
// echoing the id back can't be matched to the input. Positions are always
|
|
1507
|
+
// small integers, so they round-trip cleanly.
|
|
1508
|
+
const valid = [];
|
|
1476
1509
|
for (const id of ids) {
|
|
1477
|
-
const
|
|
1478
|
-
|
|
1479
|
-
id,
|
|
1480
|
-
success,
|
|
1481
|
-
error: success ? undefined : "Failed to delete message",
|
|
1482
|
-
});
|
|
1510
|
+
const num = Number(id);
|
|
1511
|
+
if (Number.isFinite(num))
|
|
1512
|
+
valid.push({ id, num });
|
|
1483
1513
|
}
|
|
1484
|
-
|
|
1514
|
+
if (valid.length === 0) {
|
|
1515
|
+
return ids.map((id) => ({ id, success: false, error: "Invalid message ID" }));
|
|
1516
|
+
}
|
|
1517
|
+
const script = buildAppLevelScript(`
|
|
1518
|
+
try
|
|
1519
|
+
${setup}
|
|
1520
|
+
set _out to ""
|
|
1521
|
+
set _done to {}
|
|
1522
|
+
set _ids to {${valid.map((v) => v.num).join(", ")}}
|
|
1523
|
+
set _total to count of _ids
|
|
1524
|
+
repeat with acct in accounts
|
|
1525
|
+
if (count of _done) is _total then exit repeat
|
|
1526
|
+
repeat with mb in (mailboxes of acct)
|
|
1527
|
+
if (count of _done) is _total then exit repeat
|
|
1528
|
+
repeat with _idx from 1 to _total
|
|
1529
|
+
if _idx is not in _done then
|
|
1530
|
+
set _theId to item _idx of _ids
|
|
1531
|
+
try
|
|
1532
|
+
set _m to (messages of mb whose id is _theId)
|
|
1533
|
+
if (count of _m) > 0 then
|
|
1534
|
+
set _msg to item 1 of _m
|
|
1535
|
+
${operation}
|
|
1536
|
+
set end of _done to _idx
|
|
1537
|
+
set _out to _out & (_idx as string) & "${FIELD_SEP}ok${RECORD_SEP}"
|
|
1538
|
+
end if
|
|
1539
|
+
on error _e
|
|
1540
|
+
set end of _done to _idx
|
|
1541
|
+
set _out to _out & (_idx as string) & "${FIELD_SEP}error:" & _e & "${RECORD_SEP}"
|
|
1542
|
+
end try
|
|
1543
|
+
end if
|
|
1544
|
+
end repeat
|
|
1545
|
+
end repeat
|
|
1546
|
+
end repeat
|
|
1547
|
+
repeat with _idx from 1 to _total
|
|
1548
|
+
if _idx is not in _done then set _out to _out & (_idx as string) & "${FIELD_SEP}notfound${RECORD_SEP}"
|
|
1549
|
+
end repeat
|
|
1550
|
+
return _out
|
|
1551
|
+
on error errMsg
|
|
1552
|
+
return "${BATCH_FATAL}" & errMsg
|
|
1553
|
+
end try
|
|
1554
|
+
`);
|
|
1555
|
+
// Generous timeout: one walk over the tree with indexed id probes. Scale a
|
|
1556
|
+
// little with batch size, capped.
|
|
1557
|
+
const timeoutMs = Math.min(180000, 60000 + valid.length * 500);
|
|
1558
|
+
const result = executeAppleScript(script, { timeoutMs });
|
|
1559
|
+
if (!result.success) {
|
|
1560
|
+
const err = result.error || "Batch operation failed";
|
|
1561
|
+
return ids.map((id) => ({ id, success: false, error: err }));
|
|
1562
|
+
}
|
|
1563
|
+
if (result.output.startsWith(BATCH_FATAL)) {
|
|
1564
|
+
const err = result.output.slice(BATCH_FATAL.length);
|
|
1565
|
+
return ids.map((id) => ({ id, success: false, error: err }));
|
|
1566
|
+
}
|
|
1567
|
+
// Map by-position outcomes back to the original id strings.
|
|
1568
|
+
const byId = new Map();
|
|
1569
|
+
for (const rec of result.output.split(RECORD_SEP)) {
|
|
1570
|
+
if (!rec)
|
|
1571
|
+
continue;
|
|
1572
|
+
const sep = rec.indexOf(FIELD_SEP);
|
|
1573
|
+
if (sep < 0)
|
|
1574
|
+
continue;
|
|
1575
|
+
const pos = Number(rec.slice(0, sep));
|
|
1576
|
+
const status = rec.slice(sep + FIELD_SEP.length);
|
|
1577
|
+
const entry = valid[pos - 1];
|
|
1578
|
+
if (!entry)
|
|
1579
|
+
continue;
|
|
1580
|
+
const id = entry.id;
|
|
1581
|
+
if (status === "ok") {
|
|
1582
|
+
byId.set(id, { id, success: true });
|
|
1583
|
+
}
|
|
1584
|
+
else if (status === "notfound") {
|
|
1585
|
+
byId.set(id, { id, success: false, error: "Message not found" });
|
|
1586
|
+
}
|
|
1587
|
+
else if (status.startsWith("error:")) {
|
|
1588
|
+
byId.set(id, { id, success: false, error: status.slice("error:".length) });
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
byId.set(id, { id, success: false, error: status || "Unknown error" });
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return ids.map((id) => byId.get(id) ??
|
|
1595
|
+
(Number.isFinite(Number(id))
|
|
1596
|
+
? { id, success: false, error: "No result returned" }
|
|
1597
|
+
: { id, success: false, error: "Invalid message ID" }));
|
|
1485
1598
|
}
|
|
1486
1599
|
/**
|
|
1487
|
-
*
|
|
1600
|
+
* Delete multiple messages at once (single tree walk — see runBatchOperation).
|
|
1601
|
+
*/
|
|
1602
|
+
batchDeleteMessages(ids) {
|
|
1603
|
+
return this.runBatchOperation(ids, "delete _msg");
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Move multiple messages to a mailbox at once (single tree walk).
|
|
1488
1607
|
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
* @returns Array of results for each message
|
|
1608
|
+
* The destination is resolved once (account-scoped, ambiguity-aware — a name
|
|
1609
|
+
* matching more than one mailbox fails the whole batch rather than guessing),
|
|
1610
|
+
* then every matched message is moved in the same walk.
|
|
1493
1611
|
*/
|
|
1494
1612
|
batchMoveMessages(ids, mailbox, account) {
|
|
1495
|
-
const
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1613
|
+
const targetAccount = this.resolveAccount(account);
|
|
1614
|
+
const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
|
|
1615
|
+
const safeMailbox = escapeForAppleScript(targetMailbox);
|
|
1616
|
+
const safeAccount = escapeForAppleScript(targetAccount);
|
|
1617
|
+
// Resolved once, before the walk. `mailboxes of account` is already flat
|
|
1618
|
+
// (includes nested mailboxes by path), so we match by exact name and use the
|
|
1619
|
+
// reference directly. A bad/ambiguous destination fails the whole batch.
|
|
1620
|
+
const setup = `
|
|
1621
|
+
set destName to "${safeMailbox}"
|
|
1622
|
+
set destMatches to {}
|
|
1623
|
+
repeat with _dmb in (mailboxes of account "${safeAccount}")
|
|
1624
|
+
if (name of _dmb) is destName then set end of destMatches to _dmb
|
|
1625
|
+
end repeat
|
|
1626
|
+
if (count of destMatches) is 0 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" not found in account \\"${safeAccount}\\""
|
|
1627
|
+
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"
|
|
1628
|
+
set destMailbox to item 1 of destMatches`;
|
|
1629
|
+
return this.runBatchOperation(ids, "move _msg to destMailbox", setup);
|
|
1505
1630
|
}
|
|
1506
1631
|
/**
|
|
1507
|
-
* Mark multiple messages as read at once.
|
|
1632
|
+
* Mark multiple messages as read at once (single tree walk).
|
|
1508
1633
|
*/
|
|
1509
1634
|
batchMarkAsRead(ids) {
|
|
1510
|
-
|
|
1511
|
-
for (const id of ids) {
|
|
1512
|
-
const success = this.markAsRead(id);
|
|
1513
|
-
results.push({ id, success, error: success ? undefined : "Failed to mark message as read" });
|
|
1514
|
-
}
|
|
1515
|
-
return results;
|
|
1635
|
+
return this.runBatchOperation(ids, "set read status of _msg to true");
|
|
1516
1636
|
}
|
|
1517
1637
|
/**
|
|
1518
|
-
* Mark multiple messages as unread at once.
|
|
1638
|
+
* Mark multiple messages as unread at once (single tree walk).
|
|
1519
1639
|
*/
|
|
1520
1640
|
batchMarkAsUnread(ids) {
|
|
1521
|
-
|
|
1522
|
-
for (const id of ids) {
|
|
1523
|
-
const success = this.markAsUnread(id);
|
|
1524
|
-
results.push({
|
|
1525
|
-
id,
|
|
1526
|
-
success,
|
|
1527
|
-
error: success ? undefined : "Failed to mark message as unread",
|
|
1528
|
-
});
|
|
1529
|
-
}
|
|
1530
|
-
return results;
|
|
1641
|
+
return this.runBatchOperation(ids, "set read status of _msg to false");
|
|
1531
1642
|
}
|
|
1532
1643
|
/**
|
|
1533
|
-
* Flag multiple messages at once.
|
|
1644
|
+
* Flag multiple messages at once (single tree walk).
|
|
1534
1645
|
*/
|
|
1535
1646
|
batchFlagMessages(ids) {
|
|
1536
|
-
|
|
1537
|
-
for (const id of ids) {
|
|
1538
|
-
const success = this.flagMessage(id);
|
|
1539
|
-
results.push({ id, success, error: success ? undefined : "Failed to flag message" });
|
|
1540
|
-
}
|
|
1541
|
-
return results;
|
|
1647
|
+
return this.runBatchOperation(ids, "set flagged status of _msg to true");
|
|
1542
1648
|
}
|
|
1543
1649
|
/**
|
|
1544
|
-
* Unflag multiple messages at once.
|
|
1650
|
+
* Unflag multiple messages at once (single tree walk).
|
|
1545
1651
|
*/
|
|
1546
1652
|
batchUnflagMessages(ids) {
|
|
1547
|
-
|
|
1548
|
-
for (const id of ids) {
|
|
1549
|
-
const success = this.unflagMessage(id);
|
|
1550
|
-
results.push({ id, success, error: success ? undefined : "Failed to unflag message" });
|
|
1551
|
-
}
|
|
1552
|
-
return results;
|
|
1653
|
+
return this.runBatchOperation(ids, "set flagged status of _msg to false");
|
|
1553
1654
|
}
|
|
1554
1655
|
/**
|
|
1555
1656
|
* List attachments for a message.
|
|
@@ -28,6 +28,21 @@ export interface MimeAttachmentData extends MimeAttachmentInfo {
|
|
|
28
28
|
* @returns Array of attachment metadata (name, mimeType, size)
|
|
29
29
|
*/
|
|
30
30
|
export declare function parseMimeAttachments(source: string): MimeAttachmentInfo[];
|
|
31
|
+
/**
|
|
32
|
+
* Extract the decoded `text/html` body from raw MIME source.
|
|
33
|
+
*
|
|
34
|
+
* Used by get-message's `preferHtml` path so it returns the actual HTML body
|
|
35
|
+
* rather than the entire raw MIME blob (headers + base64 attachments), which is
|
|
36
|
+
* both wrong and enormous (#32). Handles both multipart messages (walks leaf
|
|
37
|
+
* parts, descending into nested multipart/* containers) and a non-multipart
|
|
38
|
+
* message whose top-level Content-Type is text/html. Bodies are decoded per
|
|
39
|
+
* Content-Transfer-Encoding (base64 / quoted-printable / raw) and returned as
|
|
40
|
+
* UTF-8 text.
|
|
41
|
+
*
|
|
42
|
+
* @param source - Raw MIME source of the email
|
|
43
|
+
* @returns The decoded HTML body, or null if the message has no text/html part
|
|
44
|
+
*/
|
|
45
|
+
export declare function extractHtmlBody(source: string): string | null;
|
|
31
46
|
/**
|
|
32
47
|
* Extract and decode a specific attachment from MIME source by filename.
|
|
33
48
|
* Supports base64, quoted-printable, and 7bit/8bit/binary transfer encodings.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mimeParse.d.ts","sourceRoot":"","sources":["../../src/utils/mimeParse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAmB,SAAQ,kBAAkB;IAC5D,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAyLD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAyBzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,kBAAkB,GAAG,IAAI,CAwB3B"}
|
|
1
|
+
{"version":3,"file":"mimeParse.d.ts","sourceRoot":"","sources":["../../src/utils/mimeParse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAmB,SAAQ,kBAAkB;IAC5D,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAyLD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAyBzE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,kBAAkB,GAAG,IAAI,CAwB3B"}
|
package/build/utils/mimeParse.js
CHANGED
|
@@ -205,6 +205,44 @@ export function parseMimeAttachments(source) {
|
|
|
205
205
|
}
|
|
206
206
|
return attachments;
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Extract the decoded `text/html` body from raw MIME source.
|
|
210
|
+
*
|
|
211
|
+
* Used by get-message's `preferHtml` path so it returns the actual HTML body
|
|
212
|
+
* rather than the entire raw MIME blob (headers + base64 attachments), which is
|
|
213
|
+
* both wrong and enormous (#32). Handles both multipart messages (walks leaf
|
|
214
|
+
* parts, descending into nested multipart/* containers) and a non-multipart
|
|
215
|
+
* message whose top-level Content-Type is text/html. Bodies are decoded per
|
|
216
|
+
* Content-Transfer-Encoding (base64 / quoted-printable / raw) and returned as
|
|
217
|
+
* UTF-8 text.
|
|
218
|
+
*
|
|
219
|
+
* @param source - Raw MIME source of the email
|
|
220
|
+
* @returns The decoded HTML body, or null if the message has no text/html part
|
|
221
|
+
*/
|
|
222
|
+
export function extractHtmlBody(source) {
|
|
223
|
+
if (!source || !source.trim())
|
|
224
|
+
return null;
|
|
225
|
+
const boundary = extractBoundary(source);
|
|
226
|
+
if (boundary) {
|
|
227
|
+
for (const part of walkLeafParts(source, boundary)) {
|
|
228
|
+
if (extractMimeType(part.headers) === "text/html") {
|
|
229
|
+
const encoding = getHeader(part.headers, "Content-Transfer-Encoding");
|
|
230
|
+
return decodeBody(part.body, encoding).toString("utf8");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
// Non-multipart: split top headers from body and check the top Content-Type.
|
|
236
|
+
const blankLineIdx = source.search(/\r?\n\r?\n/);
|
|
237
|
+
if (blankLineIdx === -1)
|
|
238
|
+
return null;
|
|
239
|
+
const headers = source.substring(0, blankLineIdx);
|
|
240
|
+
if (extractMimeType(headers) !== "text/html")
|
|
241
|
+
return null;
|
|
242
|
+
const body = source.substring(blankLineIdx).replace(/^\r?\n\r?\n/, "");
|
|
243
|
+
const encoding = getHeader(headers, "Content-Transfer-Encoding");
|
|
244
|
+
return decodeBody(body, encoding).toString("utf8");
|
|
245
|
+
}
|
|
208
246
|
/**
|
|
209
247
|
* Extract and decode a specific attachment from MIME source by filename.
|
|
210
248
|
* Supports base64, quoted-printable, and 7bit/8bit/binary transfer encodings.
|