apple-mail-mcp 2.18.1 → 2.19.0
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 +18 -1
- package/build/cli.js +50 -3
- package/build/index.js +355 -88
- package/docs/IMAP-SETUP.md +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,6 +159,7 @@ tool, and troubleshooting. Verify any time by running the **`doctor`** tool.
|
|
|
159
159
|
| **List Messages** | List messages with pagination, sender filter, date display |
|
|
160
160
|
| **Search Messages** | Search by sender, subject, content, date range, read/flagged status — across all accounts |
|
|
161
161
|
| **Read Messages** | Get full email content (plain text or HTML) |
|
|
162
|
+
| **Read Headers** | Get a message's raw RFC 5322 headers — the author's `Date:`, Message-ID, threading ids, `Received:` trace — without downloading the body |
|
|
162
163
|
| **Send Email** | Compose and send new emails (attach by file path or inline base64 content) |
|
|
163
164
|
| **Send Serial Email** | Mail merge — send personalized emails to a list of recipients with {{placeholder}} support |
|
|
164
165
|
| **Create Draft** | Save emails to Drafts folder (attach by file path or inline base64 content) |
|
|
@@ -288,7 +289,7 @@ Get the full content of a message.
|
|
|
288
289
|
| `mailbox` | string | No | Mailbox holding the message (e.g. `"Sent Items"`). With `account`, opens that mailbox directly instead of scanning every mailbox — this is the fix for timeouts on large folders |
|
|
289
290
|
| `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
|
|
290
291
|
|
|
291
|
-
**Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available).
|
|
292
|
+
**Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available). `structuredContent` also carries `rfcMessageId` and, since 2.19.0, two dates: `dateSent` (the message's `Date:` header — Mail's `date sent`) and `dateReceived` (arrival in the mailbox — Mail's `date received` / IMAP `INTERNALDATE`). They differ legitimately by transit time; when they differ by **years**, the mailbox was migrated or re-imported and the arrival timestamp was reset — trust `dateSent` for chronology ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)).
|
|
292
293
|
|
|
293
294
|
> **Large messages / attachments:** reading a full message routes through
|
|
294
295
|
> `osascript`, whose captured output buffer defaults to **64 MB**. Override it
|
|
@@ -299,6 +300,22 @@ Get the full content of a message.
|
|
|
299
300
|
|
|
300
301
|
---
|
|
301
302
|
|
|
303
|
+
#### `get-message-headers`
|
|
304
|
+
|
|
305
|
+
Return a message's raw RFC 5322 header block — without fetching the body or any attachment — plus the parsed fields chronological and threading work needs. Added in 2.19.0 for mailboxes whose arrival timestamps were reset by a migration ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)): the `Date:` header is the author's send time and survives such moves; `INTERNALDATE` / `date received` does not.
|
|
306
|
+
|
|
307
|
+
| Parameter | Type | Required | Description |
|
|
308
|
+
|-----------|------|----------|-------------|
|
|
309
|
+
| `id` | string | Yes | Message ID (numeric or `imap:…`) |
|
|
310
|
+
| `mailbox` | string | No | Mailbox holding the message. With `account`, opens that mailbox directly instead of scanning every mailbox |
|
|
311
|
+
| `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
|
|
312
|
+
|
|
313
|
+
**Returns:** The raw header block as text. `structuredContent` carries `raw`, every header as ordered `headers[]` (`{name, value}`, folded lines joined, duplicates such as `Received:` kept in wire order, values left RFC 2047-encoded), `headerCount`, and the decoded key fields: `date` (ISO 8601, from the `Date:` header), `dateHeader` (verbatim), `dateReceived` (mailbox arrival time — IMAP `INTERNALDATE` or Mail's `date received`), `messageId`, `subject`, `from`, `to`, `cc`, `replyTo`, `inReplyTo`, `references[]` and `received[]` (first entry = last hop). Fields the message does not carry are omitted.
|
|
314
|
+
|
|
315
|
+
**Backends:** an `imap:` id fetches `BODY.PEEK[HEADER]` over IMAP (cheap even for a 20 MB message); a numeric id reads Mail's `all headers` property over AppleScript, with the same mailbox-scoped fast path as `get-message`.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
302
319
|
#### `list-messages`
|
|
303
320
|
|
|
304
321
|
List messages in a mailbox.
|
package/build/cli.js
CHANGED
|
@@ -57771,6 +57771,18 @@ var init_mimeParse = __esm({
|
|
|
57771
57771
|
}
|
|
57772
57772
|
});
|
|
57773
57773
|
|
|
57774
|
+
// src/utils/headers.ts
|
|
57775
|
+
function isoOrUndefined(d) {
|
|
57776
|
+
if (d === void 0 || d === "") return void 0;
|
|
57777
|
+
const date = d instanceof Date ? d : new Date(d);
|
|
57778
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
57779
|
+
}
|
|
57780
|
+
var init_headers = __esm({
|
|
57781
|
+
"src/utils/headers.ts"() {
|
|
57782
|
+
"use strict";
|
|
57783
|
+
}
|
|
57784
|
+
});
|
|
57785
|
+
|
|
57774
57786
|
// src/services/auditLog.ts
|
|
57775
57787
|
function classifyCountStatus(readable, expected, observed) {
|
|
57776
57788
|
if (!readable) return { status: "unknown", unknownReason: "count-unreadable" };
|
|
@@ -57812,6 +57824,7 @@ __export(imapClient_exports, {
|
|
|
57812
57824
|
imapFetchMessageId: () => imapFetchMessageId,
|
|
57813
57825
|
imapFlagMessage: () => imapFlagMessage,
|
|
57814
57826
|
imapGetMessage: () => imapGetMessage,
|
|
57827
|
+
imapGetMessageHeaders: () => imapGetMessageHeaders,
|
|
57815
57828
|
imapGetMessageSource: () => imapGetMessageSource,
|
|
57816
57829
|
imapHealthCheck: () => imapHealthCheck,
|
|
57817
57830
|
imapListAttachments: () => imapListAttachments,
|
|
@@ -58604,7 +58617,10 @@ async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
|
58604
58617
|
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
58605
58618
|
const msg = await client.fetchOne(
|
|
58606
58619
|
String(ref.uid),
|
|
58607
|
-
|
|
58620
|
+
// INTERNALDATE rides along so the read can report the server's arrival
|
|
58621
|
+
// time beside the author's `Date:` header (#224). The envelope's `date`
|
|
58622
|
+
// IS the `Date:` header — imapflow builds ENVELOPE from the header block.
|
|
58623
|
+
{ envelope: true, internalDate: true, source: true },
|
|
58608
58624
|
{ uid: true }
|
|
58609
58625
|
);
|
|
58610
58626
|
if (!msg)
|
|
@@ -58612,9 +58628,39 @@ async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
|
58612
58628
|
const subject = msg.envelope?.subject || "(no subject)";
|
|
58613
58629
|
const src = msg.source ? msg.source.toString() : "";
|
|
58614
58630
|
const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ?? extractTextBody(src) ?? extractHtmlBody(src) ?? "(no readable body)";
|
|
58615
|
-
return {
|
|
58631
|
+
return {
|
|
58632
|
+
success: true,
|
|
58633
|
+
info: `Subject: ${subject}
|
|
58616
58634
|
|
|
58617
|
-
${body}
|
|
58635
|
+
${body}`,
|
|
58636
|
+
meta: {
|
|
58637
|
+
dateSent: isoOrUndefined(msg.envelope?.date),
|
|
58638
|
+
dateReceived: isoOrUndefined(msg.internalDate),
|
|
58639
|
+
// The envelope carries the Message-ID; `info` deliberately does not (it
|
|
58640
|
+
// is subject + body), so the caller could never recover it from there.
|
|
58641
|
+
rfcMessageId: msg.envelope?.messageId ? normalizeMessageId(msg.envelope.messageId) : ""
|
|
58642
|
+
}
|
|
58643
|
+
};
|
|
58644
|
+
});
|
|
58645
|
+
}
|
|
58646
|
+
async function imapGetMessageHeaders(id, deps = {}) {
|
|
58647
|
+
const ref = decodeImapId(id);
|
|
58648
|
+
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
58649
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
58650
|
+
const msg = await client.fetchOne(
|
|
58651
|
+
String(ref.uid),
|
|
58652
|
+
{ envelope: true, internalDate: true, headers: true },
|
|
58653
|
+
{ uid: true }
|
|
58654
|
+
);
|
|
58655
|
+
if (!msg)
|
|
58656
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
58657
|
+
const raw = msg.headers ? msg.headers.toString() : "";
|
|
58658
|
+
if (!raw.trim()) return { success: false, error: "IMAP returned no header block." };
|
|
58659
|
+
return {
|
|
58660
|
+
success: true,
|
|
58661
|
+
info: raw,
|
|
58662
|
+
meta: { dateReceived: isoOrUndefined(msg.internalDate) }
|
|
58663
|
+
};
|
|
58618
58664
|
});
|
|
58619
58665
|
}
|
|
58620
58666
|
function normalizeMessageId(mid) {
|
|
@@ -59054,6 +59100,7 @@ var init_imapClient = __esm({
|
|
|
59054
59100
|
init_smtpMailer();
|
|
59055
59101
|
init_docsUrls();
|
|
59056
59102
|
init_mimeParse();
|
|
59103
|
+
init_headers();
|
|
59057
59104
|
init_auditLog();
|
|
59058
59105
|
init_attachmentLimits();
|
|
59059
59106
|
IMAP_ENV = {
|
package/build/index.js
CHANGED
|
@@ -65092,6 +65092,122 @@ var require_imap_flow = __commonJS({
|
|
|
65092
65092
|
}
|
|
65093
65093
|
});
|
|
65094
65094
|
|
|
65095
|
+
// src/utils/headers.ts
|
|
65096
|
+
function decodeEncodedWords(value) {
|
|
65097
|
+
if (!value.includes("=?")) return value;
|
|
65098
|
+
const joined = value.replace(/(\?=)\s+(=\?)/g, "$1$2");
|
|
65099
|
+
return joined.replace(
|
|
65100
|
+
/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/g,
|
|
65101
|
+
(whole, charset, enc, text) => {
|
|
65102
|
+
try {
|
|
65103
|
+
const bytes = enc.toUpperCase() === "B" ? Buffer.from(text, "base64") : Buffer.from(
|
|
65104
|
+
text.replace(/_/g, " ").replace(
|
|
65105
|
+
/=([0-9A-Fa-f]{2})/g,
|
|
65106
|
+
(_m, h) => String.fromCharCode(parseInt(h, 16))
|
|
65107
|
+
),
|
|
65108
|
+
"latin1"
|
|
65109
|
+
);
|
|
65110
|
+
return new TextDecoder(normalizeCharset(charset)).decode(bytes);
|
|
65111
|
+
} catch {
|
|
65112
|
+
return whole;
|
|
65113
|
+
}
|
|
65114
|
+
}
|
|
65115
|
+
);
|
|
65116
|
+
}
|
|
65117
|
+
function normalizeCharset(charset) {
|
|
65118
|
+
const bare = charset.split("*")[0].trim().toLowerCase();
|
|
65119
|
+
try {
|
|
65120
|
+
new TextDecoder(bare);
|
|
65121
|
+
return bare;
|
|
65122
|
+
} catch {
|
|
65123
|
+
return "utf-8";
|
|
65124
|
+
}
|
|
65125
|
+
}
|
|
65126
|
+
function bareId(raw) {
|
|
65127
|
+
return raw.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
65128
|
+
}
|
|
65129
|
+
function splitIds(raw) {
|
|
65130
|
+
const bracketed = raw.match(/<[^>]+>/g);
|
|
65131
|
+
if (bracketed) return bracketed.map(bareId).filter(Boolean);
|
|
65132
|
+
return raw.split(/[\s,]+/).map(bareId).filter(Boolean);
|
|
65133
|
+
}
|
|
65134
|
+
function parseHeaderBlock(input) {
|
|
65135
|
+
const text = (input ?? "").replace(/\r\n/g, "\n");
|
|
65136
|
+
const blank = text.search(/\n\n/);
|
|
65137
|
+
const raw = (blank === -1 ? text : text.slice(0, blank)).replace(/\n+$/, "");
|
|
65138
|
+
const headers = [];
|
|
65139
|
+
for (const line of raw.split("\n")) {
|
|
65140
|
+
if (/^[ \t]/.test(line) && headers.length) {
|
|
65141
|
+
headers[headers.length - 1].value += " " + line.trim();
|
|
65142
|
+
continue;
|
|
65143
|
+
}
|
|
65144
|
+
const m = /^([!-9;-~]+):[ \t]?(.*)$/.exec(line);
|
|
65145
|
+
if (!m) continue;
|
|
65146
|
+
headers.push({ name: m[1], value: m[2].trim() });
|
|
65147
|
+
}
|
|
65148
|
+
const first = (name) => headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value;
|
|
65149
|
+
const all = (name) => headers.filter((h) => h.name.toLowerCase() === name.toLowerCase()).map((h) => h.value);
|
|
65150
|
+
const decoded = (name) => {
|
|
65151
|
+
const v = first(name);
|
|
65152
|
+
return v === void 0 ? void 0 : decodeEncodedWords(v);
|
|
65153
|
+
};
|
|
65154
|
+
const dateHeader = first("Date");
|
|
65155
|
+
let date3;
|
|
65156
|
+
if (dateHeader) {
|
|
65157
|
+
const parsed = new Date(dateHeader.replace(/\s*\([^)]*\)\s*$/, ""));
|
|
65158
|
+
if (!Number.isNaN(parsed.getTime())) date3 = parsed.toISOString();
|
|
65159
|
+
}
|
|
65160
|
+
const messageIdRaw = first("Message-ID") ?? first("Message-Id");
|
|
65161
|
+
const inReplyToRaw = first("In-Reply-To");
|
|
65162
|
+
const referencesRaw = first("References");
|
|
65163
|
+
return {
|
|
65164
|
+
raw,
|
|
65165
|
+
headers,
|
|
65166
|
+
date: date3,
|
|
65167
|
+
dateHeader,
|
|
65168
|
+
messageId: messageIdRaw ? bareId(messageIdRaw) || void 0 : void 0,
|
|
65169
|
+
subject: decoded("Subject"),
|
|
65170
|
+
from: decoded("From"),
|
|
65171
|
+
to: decoded("To"),
|
|
65172
|
+
cc: decoded("Cc"),
|
|
65173
|
+
replyTo: decoded("Reply-To"),
|
|
65174
|
+
inReplyTo: inReplyToRaw ? bareId(inReplyToRaw) || void 0 : void 0,
|
|
65175
|
+
references: referencesRaw ? splitIds(referencesRaw) : [],
|
|
65176
|
+
received: all("Received")
|
|
65177
|
+
};
|
|
65178
|
+
}
|
|
65179
|
+
function headersStructured(id, parsed, dateReceived) {
|
|
65180
|
+
const received = dateReceived instanceof Date ? Number.isNaN(dateReceived.getTime()) ? void 0 : dateReceived.toISOString() : dateReceived || void 0;
|
|
65181
|
+
return {
|
|
65182
|
+
id,
|
|
65183
|
+
raw: parsed.raw,
|
|
65184
|
+
headers: parsed.headers,
|
|
65185
|
+
headerCount: parsed.headers.length,
|
|
65186
|
+
...parsed.date !== void 0 ? { date: parsed.date } : {},
|
|
65187
|
+
...parsed.dateHeader !== void 0 ? { dateHeader: parsed.dateHeader } : {},
|
|
65188
|
+
...received !== void 0 ? { dateReceived: received } : {},
|
|
65189
|
+
...parsed.messageId !== void 0 ? { messageId: parsed.messageId } : {},
|
|
65190
|
+
...parsed.subject !== void 0 ? { subject: parsed.subject } : {},
|
|
65191
|
+
...parsed.from !== void 0 ? { from: parsed.from } : {},
|
|
65192
|
+
...parsed.to !== void 0 ? { to: parsed.to } : {},
|
|
65193
|
+
...parsed.cc !== void 0 ? { cc: parsed.cc } : {},
|
|
65194
|
+
...parsed.replyTo !== void 0 ? { replyTo: parsed.replyTo } : {},
|
|
65195
|
+
...parsed.inReplyTo !== void 0 ? { inReplyTo: parsed.inReplyTo } : {},
|
|
65196
|
+
references: parsed.references,
|
|
65197
|
+
received: parsed.received
|
|
65198
|
+
};
|
|
65199
|
+
}
|
|
65200
|
+
function isoOrUndefined(d) {
|
|
65201
|
+
if (d === void 0 || d === "") return void 0;
|
|
65202
|
+
const date3 = d instanceof Date ? d : new Date(d);
|
|
65203
|
+
return Number.isNaN(date3.getTime()) ? void 0 : date3.toISOString();
|
|
65204
|
+
}
|
|
65205
|
+
var init_headers = __esm({
|
|
65206
|
+
"src/utils/headers.ts"() {
|
|
65207
|
+
"use strict";
|
|
65208
|
+
}
|
|
65209
|
+
});
|
|
65210
|
+
|
|
65095
65211
|
// src/services/imapClient.ts
|
|
65096
65212
|
var imapClient_exports = {};
|
|
65097
65213
|
__export(imapClient_exports, {
|
|
@@ -65118,6 +65234,7 @@ __export(imapClient_exports, {
|
|
|
65118
65234
|
imapFetchMessageId: () => imapFetchMessageId,
|
|
65119
65235
|
imapFlagMessage: () => imapFlagMessage,
|
|
65120
65236
|
imapGetMessage: () => imapGetMessage,
|
|
65237
|
+
imapGetMessageHeaders: () => imapGetMessageHeaders,
|
|
65121
65238
|
imapGetMessageSource: () => imapGetMessageSource,
|
|
65122
65239
|
imapHealthCheck: () => imapHealthCheck,
|
|
65123
65240
|
imapListAttachments: () => imapListAttachments,
|
|
@@ -65910,7 +66027,10 @@ async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
|
65910
66027
|
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
65911
66028
|
const msg = await client.fetchOne(
|
|
65912
66029
|
String(ref.uid),
|
|
65913
|
-
|
|
66030
|
+
// INTERNALDATE rides along so the read can report the server's arrival
|
|
66031
|
+
// time beside the author's `Date:` header (#224). The envelope's `date`
|
|
66032
|
+
// IS the `Date:` header — imapflow builds ENVELOPE from the header block.
|
|
66033
|
+
{ envelope: true, internalDate: true, source: true },
|
|
65914
66034
|
{ uid: true }
|
|
65915
66035
|
);
|
|
65916
66036
|
if (!msg)
|
|
@@ -65918,9 +66038,39 @@ async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
|
65918
66038
|
const subject = msg.envelope?.subject || "(no subject)";
|
|
65919
66039
|
const src = msg.source ? msg.source.toString() : "";
|
|
65920
66040
|
const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ?? extractTextBody(src) ?? extractHtmlBody(src) ?? "(no readable body)";
|
|
65921
|
-
return {
|
|
66041
|
+
return {
|
|
66042
|
+
success: true,
|
|
66043
|
+
info: `Subject: ${subject}
|
|
65922
66044
|
|
|
65923
|
-
${body}
|
|
66045
|
+
${body}`,
|
|
66046
|
+
meta: {
|
|
66047
|
+
dateSent: isoOrUndefined(msg.envelope?.date),
|
|
66048
|
+
dateReceived: isoOrUndefined(msg.internalDate),
|
|
66049
|
+
// The envelope carries the Message-ID; `info` deliberately does not (it
|
|
66050
|
+
// is subject + body), so the caller could never recover it from there.
|
|
66051
|
+
rfcMessageId: msg.envelope?.messageId ? normalizeMessageId(msg.envelope.messageId) : ""
|
|
66052
|
+
}
|
|
66053
|
+
};
|
|
66054
|
+
});
|
|
66055
|
+
}
|
|
66056
|
+
async function imapGetMessageHeaders(id, deps = {}) {
|
|
66057
|
+
const ref = decodeImapId(id);
|
|
66058
|
+
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
66059
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
66060
|
+
const msg = await client.fetchOne(
|
|
66061
|
+
String(ref.uid),
|
|
66062
|
+
{ envelope: true, internalDate: true, headers: true },
|
|
66063
|
+
{ uid: true }
|
|
66064
|
+
);
|
|
66065
|
+
if (!msg)
|
|
66066
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
66067
|
+
const raw = msg.headers ? msg.headers.toString() : "";
|
|
66068
|
+
if (!raw.trim()) return { success: false, error: "IMAP returned no header block." };
|
|
66069
|
+
return {
|
|
66070
|
+
success: true,
|
|
66071
|
+
info: raw,
|
|
66072
|
+
meta: { dateReceived: isoOrUndefined(msg.internalDate) }
|
|
66073
|
+
};
|
|
65924
66074
|
});
|
|
65925
66075
|
}
|
|
65926
66076
|
function normalizeMessageId(mid) {
|
|
@@ -66360,6 +66510,7 @@ var init_imapClient = __esm({
|
|
|
66360
66510
|
init_smtpMailer();
|
|
66361
66511
|
init_docsUrls();
|
|
66362
66512
|
init_mimeParse();
|
|
66513
|
+
init_headers();
|
|
66363
66514
|
init_auditLog();
|
|
66364
66515
|
init_attachmentLimits();
|
|
66365
66516
|
IMAP_ENV = {
|
|
@@ -81688,6 +81839,7 @@ var DIAG_FIELD_SEP = "F";
|
|
|
81688
81839
|
var DIAG_ITEM_SEP = "M";
|
|
81689
81840
|
var CONTENT_MARKER = "CONTENT";
|
|
81690
81841
|
var MSGID_MARKER = "MSGID";
|
|
81842
|
+
var DATES_MARKER = "DATES";
|
|
81691
81843
|
var HTML_MARKER = "HTML";
|
|
81692
81844
|
var LOOKUP_ERROR_MARKER = "ERR";
|
|
81693
81845
|
var BATCH_FATAL = "FATAL";
|
|
@@ -81819,6 +81971,26 @@ function buildAttachmentCommands(attachments) {
|
|
|
81819
81971
|
return commands;
|
|
81820
81972
|
}
|
|
81821
81973
|
var AS_DATE_TO_STRING = `((year of d) as string) & "-" & ((month of d as integer) as string) & "-" & ((day of d) as string) & "-" & ((hours of d) as string) & "-" & ((minutes of d) as string) & "-" & ((seconds of d) as string)`;
|
|
81974
|
+
var AS_MESSAGE_DATES_FRAGMENT = `set msgDateSent to ""
|
|
81975
|
+
try
|
|
81976
|
+
set d to date sent of msg
|
|
81977
|
+
set msgDateSent to ${AS_DATE_TO_STRING}
|
|
81978
|
+
end try
|
|
81979
|
+
set msgDateRecv to ""
|
|
81980
|
+
try
|
|
81981
|
+
set d to date received of msg
|
|
81982
|
+
set msgDateRecv to ${AS_DATE_TO_STRING}
|
|
81983
|
+
end try
|
|
81984
|
+
set msgDates to msgDateSent & "|" & msgDateRecv`;
|
|
81985
|
+
function parseMessageDates(pair) {
|
|
81986
|
+
const [sent = "", received = ""] = pair.split("|");
|
|
81987
|
+
const parse3 = (v) => {
|
|
81988
|
+
if (!v.trim()) return void 0;
|
|
81989
|
+
const d = parseAppleScriptDate(v.trim());
|
|
81990
|
+
return Number.isNaN(d.getTime()) ? void 0 : d;
|
|
81991
|
+
};
|
|
81992
|
+
return { dateSent: parse3(sent), dateReceived: parse3(received) };
|
|
81993
|
+
}
|
|
81822
81994
|
function buildMessageRowLoop(opts) {
|
|
81823
81995
|
const { collection, limit, dedup, dateFilter, trailing = "", offset, withAttachments } = opts;
|
|
81824
81996
|
const dedupOpen = dedup ? `if seenIds does not contain msgId then
|
|
@@ -83305,6 +83477,55 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
83305
83477
|
hasAttachments: parts.length > 9 ? parts[9] === "true" : false
|
|
83306
83478
|
};
|
|
83307
83479
|
}
|
|
83480
|
+
/**
|
|
83481
|
+
* The unscoped by-id resolution: walk every mailbox of every account, then the
|
|
83482
|
+
* local store (#183), and run `innerAction` with `msg` bound when EXACTLY one
|
|
83483
|
+
* mailbox holds the id — `{LOOKUP_ERROR_MARKER}` prefixes both the not-found
|
|
83484
|
+
* and the ambiguous outcome. Shared by getMessageContent, getRawSource and
|
|
83485
|
+
* getMessageHeaders so the three reads cannot drift apart (the pre-#224 code
|
|
83486
|
+
* carried two byte-identical copies).
|
|
83487
|
+
*/
|
|
83488
|
+
unscopedByIdScript(id, innerAction) {
|
|
83489
|
+
return buildAppLevelScript(`
|
|
83490
|
+
try
|
|
83491
|
+
set _hits to {}
|
|
83492
|
+
set _names to ""
|
|
83493
|
+
repeat with acct in accounts
|
|
83494
|
+
repeat with mb in mailboxes of acct
|
|
83495
|
+
try
|
|
83496
|
+
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
83497
|
+
if (count of matchingMsgs) > 0 then
|
|
83498
|
+
set end of _hits to item 1 of matchingMsgs
|
|
83499
|
+
set _names to _names & (name of acct) & "/" & (name of mb) & ", "
|
|
83500
|
+
end if
|
|
83501
|
+
end try
|
|
83502
|
+
end repeat
|
|
83503
|
+
end repeat
|
|
83504
|
+
-- #183: local mailboxes belong to no account, so the walk above cannot
|
|
83505
|
+
-- reach them. Collect into the SAME _hits/_names, which means an id
|
|
83506
|
+
-- present both in an account and locally is now correctly reported as
|
|
83507
|
+
-- ambiguous rather than silently resolving to the account copy.${localMailboxBindingFragment()}
|
|
83508
|
+
repeat with mb in _mbs
|
|
83509
|
+
try
|
|
83510
|
+
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
83511
|
+
if (count of matchingMsgs) > 0 then
|
|
83512
|
+
set end of _hits to item 1 of matchingMsgs
|
|
83513
|
+
set _names to _names & "${LOCAL_STORE_LABEL}/" & (name of mb) & ", "
|
|
83514
|
+
end if
|
|
83515
|
+
end try
|
|
83516
|
+
end repeat
|
|
83517
|
+
if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
|
|
83518
|
+
if (count of _hits) > 1 then return "${LOOKUP_ERROR_MARKER}${AMBIGUOUS_ID_PREFIX}${Number(id)} is present in more than one mailbox (" & _names & "); list or search that mailbox first so the read targets the right copy"
|
|
83519
|
+
if (count of _hits) is 1 then
|
|
83520
|
+
set msg to item 1 of _hits
|
|
83521
|
+
${innerAction}
|
|
83522
|
+
end if
|
|
83523
|
+
return ""
|
|
83524
|
+
on error errMsg
|
|
83525
|
+
return ""
|
|
83526
|
+
end try
|
|
83527
|
+
`);
|
|
83528
|
+
}
|
|
83308
83529
|
/**
|
|
83309
83530
|
* Build an app-level AppleScript that opens exactly one account+mailbox, finds
|
|
83310
83531
|
* the message with numeric `id` in it, and runs `innerAction` (which may assume
|
|
@@ -83384,7 +83605,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
83384
83605
|
end try
|
|
83385
83606
|
set msgContent to content of msg
|
|
83386
83607
|
${sourceFetch}
|
|
83387
|
-
|
|
83608
|
+
${AS_MESSAGE_DATES_FRAGMENT}
|
|
83609
|
+
return msgSubject & "${MSGID_MARKER}" & msgRfcId & "${DATES_MARKER}" & msgDates & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlSource`;
|
|
83388
83610
|
const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
|
|
83389
83611
|
if (loc) {
|
|
83390
83612
|
const scopedScript = this.scopedByIdScript(loc.account, loc.mailbox, id, innerFetch);
|
|
@@ -83395,45 +83617,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
83395
83617
|
);
|
|
83396
83618
|
if (scoped) return scoped;
|
|
83397
83619
|
}
|
|
83398
|
-
const script =
|
|
83399
|
-
try
|
|
83400
|
-
set _hits to {}
|
|
83401
|
-
set _names to ""
|
|
83402
|
-
repeat with acct in accounts
|
|
83403
|
-
repeat with mb in mailboxes of acct
|
|
83404
|
-
try
|
|
83405
|
-
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
83406
|
-
if (count of matchingMsgs) > 0 then
|
|
83407
|
-
set end of _hits to item 1 of matchingMsgs
|
|
83408
|
-
set _names to _names & (name of acct) & "/" & (name of mb) & ", "
|
|
83409
|
-
end if
|
|
83410
|
-
end try
|
|
83411
|
-
end repeat
|
|
83412
|
-
end repeat
|
|
83413
|
-
-- #183: local mailboxes belong to no account, so the walk above cannot
|
|
83414
|
-
-- reach them. Collect into the SAME _hits/_names, which means an id
|
|
83415
|
-
-- present both in an account and locally is now correctly reported as
|
|
83416
|
-
-- ambiguous rather than silently resolving to the account copy.${localMailboxBindingFragment()}
|
|
83417
|
-
repeat with mb in _mbs
|
|
83418
|
-
try
|
|
83419
|
-
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
83420
|
-
if (count of matchingMsgs) > 0 then
|
|
83421
|
-
set end of _hits to item 1 of matchingMsgs
|
|
83422
|
-
set _names to _names & "${LOCAL_STORE_LABEL}/" & (name of mb) & ", "
|
|
83423
|
-
end if
|
|
83424
|
-
end try
|
|
83425
|
-
end repeat
|
|
83426
|
-
if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
|
|
83427
|
-
if (count of _hits) > 1 then return "${LOOKUP_ERROR_MARKER}${AMBIGUOUS_ID_PREFIX}${Number(id)} is present in more than one mailbox (" & _names & "); list or search that mailbox first so the read targets the right copy"
|
|
83428
|
-
if (count of _hits) is 1 then
|
|
83429
|
-
set msg to item 1 of _hits
|
|
83430
|
-
${innerFetch}
|
|
83431
|
-
end if
|
|
83432
|
-
return ""
|
|
83433
|
-
on error errMsg
|
|
83434
|
-
return ""
|
|
83435
|
-
end try
|
|
83436
|
-
`);
|
|
83620
|
+
const script = this.unscopedByIdScript(id, innerFetch);
|
|
83437
83621
|
return this.parseMessageContent(
|
|
83438
83622
|
id,
|
|
83439
83623
|
executeAppleScript(script, { timeoutMs: 6e4 }),
|
|
@@ -83461,15 +83645,62 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
83461
83645
|
if (parts.length < 2) return null;
|
|
83462
83646
|
const subjParts = parts[0].split(MSGID_MARKER);
|
|
83463
83647
|
const subject = subjParts[0];
|
|
83464
|
-
const
|
|
83648
|
+
const idAndDates = (subjParts.length > 1 ? subjParts[1] : "").split(DATES_MARKER);
|
|
83649
|
+
const rfcMessageId = normalizeRfcMessageId(idAndDates[0]);
|
|
83650
|
+
const { dateSent, dateReceived } = parseMessageDates(idAndDates[1] ?? "");
|
|
83465
83651
|
const htmlContent = includeHtml && rawSource ? extractHtmlBody(rawSource) || void 0 : void 0;
|
|
83466
83652
|
return {
|
|
83467
83653
|
id: id.toString(),
|
|
83468
83654
|
subject,
|
|
83469
83655
|
plainText: parts[1],
|
|
83470
83656
|
htmlContent,
|
|
83471
|
-
rfcMessageId
|
|
83657
|
+
rfcMessageId,
|
|
83658
|
+
...dateSent ? { dateSent } : {},
|
|
83659
|
+
...dateReceived ? { dateReceived } : {}
|
|
83660
|
+
};
|
|
83661
|
+
}
|
|
83662
|
+
/**
|
|
83663
|
+
* Fetch ONLY the raw RFC 5322 header block of a message (#224) — Mail's
|
|
83664
|
+
* `all headers` property — plus its `date received`, so the caller can show the
|
|
83665
|
+
* arrival timestamp beside the author's `Date:` header. Same scoped-fast-path /
|
|
83666
|
+
* full-scan resolution as getMessageContent; never reads the body or source.
|
|
83667
|
+
* Returns null (with `lastMessageLookupError` set when Mail said why) on a miss.
|
|
83668
|
+
*/
|
|
83669
|
+
getMessageHeaders(id, hint) {
|
|
83670
|
+
this.lastMessageLookupError = void 0;
|
|
83671
|
+
const innerFetch = `
|
|
83672
|
+
set msgHeaders to ""
|
|
83673
|
+
try
|
|
83674
|
+
set msgHeaders to all headers of msg
|
|
83675
|
+
end try
|
|
83676
|
+
${AS_MESSAGE_DATES_FRAGMENT}
|
|
83677
|
+
return msgDates & "${DATES_MARKER}" & msgHeaders`;
|
|
83678
|
+
const parse3 = (result) => {
|
|
83679
|
+
if (!result.success || !result.output.trim()) {
|
|
83680
|
+
if (!result.success) console.error(`Failed to get message headers: ${result.error}`);
|
|
83681
|
+
return null;
|
|
83682
|
+
}
|
|
83683
|
+
if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
83684
|
+
this.lastMessageLookupError = result.output.slice(LOOKUP_ERROR_MARKER.length).trim();
|
|
83685
|
+
return null;
|
|
83686
|
+
}
|
|
83687
|
+
const idx = result.output.indexOf(DATES_MARKER);
|
|
83688
|
+
if (idx === -1) return null;
|
|
83689
|
+
const { dateReceived } = parseMessageDates(result.output.slice(0, idx));
|
|
83690
|
+
const raw = result.output.slice(idx + DATES_MARKER.length);
|
|
83691
|
+
if (!raw.trim()) return null;
|
|
83692
|
+
return { raw, ...dateReceived ? { dateReceived } : {} };
|
|
83472
83693
|
};
|
|
83694
|
+
const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
|
|
83695
|
+
if (loc) {
|
|
83696
|
+
const scoped = parse3(
|
|
83697
|
+
executeAppleScript(this.scopedByIdScript(loc.account, loc.mailbox, id, innerFetch), {
|
|
83698
|
+
timeoutMs: 6e4
|
|
83699
|
+
})
|
|
83700
|
+
);
|
|
83701
|
+
if (scoped) return scoped;
|
|
83702
|
+
}
|
|
83703
|
+
return parse3(executeAppleScript(this.unscopedByIdScript(id, innerFetch), { timeoutMs: 6e4 }));
|
|
83473
83704
|
}
|
|
83474
83705
|
/**
|
|
83475
83706
|
* Get the raw MIME source of a message.
|
|
@@ -83498,45 +83729,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
83498
83729
|
this.lastMessageLookupError = scoped.output.slice(LOOKUP_ERROR_MARKER.length).trim();
|
|
83499
83730
|
}
|
|
83500
83731
|
}
|
|
83501
|
-
const script =
|
|
83502
|
-
try
|
|
83503
|
-
set _hits to {}
|
|
83504
|
-
set _names to ""
|
|
83505
|
-
repeat with acct in accounts
|
|
83506
|
-
repeat with mb in mailboxes of acct
|
|
83507
|
-
try
|
|
83508
|
-
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
83509
|
-
if (count of matchingMsgs) > 0 then
|
|
83510
|
-
set end of _hits to item 1 of matchingMsgs
|
|
83511
|
-
set _names to _names & (name of acct) & "/" & (name of mb) & ", "
|
|
83512
|
-
end if
|
|
83513
|
-
end try
|
|
83514
|
-
end repeat
|
|
83515
|
-
end repeat
|
|
83516
|
-
-- #183: local mailboxes belong to no account, so the walk above cannot
|
|
83517
|
-
-- reach them. Collect into the SAME _hits/_names, which means an id
|
|
83518
|
-
-- present both in an account and locally is now correctly reported as
|
|
83519
|
-
-- ambiguous rather than silently resolving to the account copy.${localMailboxBindingFragment()}
|
|
83520
|
-
repeat with mb in _mbs
|
|
83521
|
-
try
|
|
83522
|
-
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
83523
|
-
if (count of matchingMsgs) > 0 then
|
|
83524
|
-
set end of _hits to item 1 of matchingMsgs
|
|
83525
|
-
set _names to _names & "${LOCAL_STORE_LABEL}/" & (name of mb) & ", "
|
|
83526
|
-
end if
|
|
83527
|
-
end try
|
|
83528
|
-
end repeat
|
|
83529
|
-
if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found"
|
|
83530
|
-
if (count of _hits) > 1 then return "${LOOKUP_ERROR_MARKER}${AMBIGUOUS_ID_PREFIX}${Number(id)} is present in more than one mailbox (" & _names & "); list or search that mailbox first so the read targets the right copy"
|
|
83531
|
-
if (count of _hits) is 1 then
|
|
83532
|
-
set msg to item 1 of _hits
|
|
83533
|
-
return source of msg
|
|
83534
|
-
end if
|
|
83535
|
-
return ""
|
|
83536
|
-
on error errMsg
|
|
83537
|
-
return ""
|
|
83538
|
-
end try
|
|
83539
|
-
`);
|
|
83732
|
+
const script = this.unscopedByIdScript(id, "return source of msg");
|
|
83540
83733
|
const result = executeAppleScript(script, { timeoutMs: 12e4 });
|
|
83541
83734
|
if (!result.success || !result.output.trim()) {
|
|
83542
83735
|
return null;
|
|
@@ -87082,6 +87275,7 @@ function subjectFromGetMessage(info) {
|
|
|
87082
87275
|
|
|
87083
87276
|
// src/index.ts
|
|
87084
87277
|
init_mimeParse();
|
|
87278
|
+
init_headers();
|
|
87085
87279
|
|
|
87086
87280
|
// src/services/imapIdle.ts
|
|
87087
87281
|
var import_imapflow2 = __toESM(require_imap_flow(), 1);
|
|
@@ -87635,9 +87829,9 @@ registerTool(
|
|
|
87635
87829
|
"get-message",
|
|
87636
87830
|
{
|
|
87637
87831
|
description: `Use when: reading the full body of one message whose id you already have (numeric or imap:\u2026); set preferHtml to get the HTML body instead of plain text.
|
|
87638
|
-
Returns: the message subject, body (plain text by default, HTML when preferHtml is true),
|
|
87832
|
+
Returns: the message subject, body (plain text by default, HTML when preferHtml is true), its stable RFC Message-ID (rfcMessageId) for dedup/threading, and two dates: dateSent (the author's Date: header \u2014 survives a migration/re-import) and dateReceived (arrival in the mailbox; this is the one a migration resets).
|
|
87639
87833
|
Tip: pass the mailbox+account you got the id from (e.g. from search-messages) to fetch it directly \u2014 required for reliable reads of large folders like "Sent Items", which otherwise time out.
|
|
87640
|
-
Do not use when: you don't yet have an id (use search-messages or list-messages first),
|
|
87834
|
+
Do not use when: you don't yet have an id (use search-messages or list-messages first), you want the whole conversation (use get-thread), or you need the raw headers / Received: trace (use get-message-headers).`,
|
|
87641
87835
|
inputSchema: {
|
|
87642
87836
|
id: MESSAGE_ID_SCHEMA,
|
|
87643
87837
|
preferHtml: external_exports.boolean().optional().describe("Return the HTML body (extracted from the message source) instead of plain text"),
|
|
@@ -87655,6 +87849,12 @@ Do not use when: you don't yet have an id (use search-messages or list-messages
|
|
|
87655
87849
|
isHtml: external_exports.boolean().optional(),
|
|
87656
87850
|
rfcMessageId: external_exports.string().optional().describe(
|
|
87657
87851
|
"Stable RFC 5322 Message-ID (angle brackets stripped); empty when the message has none"
|
|
87852
|
+
),
|
|
87853
|
+
dateSent: external_exports.string().optional().describe(
|
|
87854
|
+
"ISO 8601 send time from the message's Date: header (Mail's `date sent`). Absent when the message carries no parseable Date: header."
|
|
87855
|
+
),
|
|
87856
|
+
dateReceived: external_exports.string().optional().describe(
|
|
87857
|
+
"ISO 8601 arrival time in the mailbox (IMAP INTERNALDATE / Mail's `date received`). A migration or re-import resets this; compare with dateSent."
|
|
87658
87858
|
)
|
|
87659
87859
|
}
|
|
87660
87860
|
},
|
|
@@ -87672,7 +87872,13 @@ Do not use when: you don't yet have an id (use search-messages or list-messages
|
|
|
87672
87872
|
subject: subjectFromGetMessage(r.info),
|
|
87673
87873
|
body: sep3 >= 0 ? r.info.slice(sep3 + 2) : r.info,
|
|
87674
87874
|
isHtml: preferHtml === true,
|
|
87675
|
-
|
|
87875
|
+
// Prefer the envelope's Message-ID (#224 fix): `info` is subject +
|
|
87876
|
+
// body with no header block, so parsing it yielded "" for every
|
|
87877
|
+
// IMAP-sourced message from 2.2.0 through 2.18.1.
|
|
87878
|
+
rfcMessageId: r.meta?.rfcMessageId || extractRfcMessageIdFromSource(r.info),
|
|
87879
|
+
// #224: envelope Date: header and INTERNALDATE, already ISO strings.
|
|
87880
|
+
...r.meta?.dateSent ? { dateSent: r.meta.dateSent } : {},
|
|
87881
|
+
...r.meta?.dateReceived ? { dateReceived: r.meta.dateReceived } : {}
|
|
87676
87882
|
};
|
|
87677
87883
|
},
|
|
87678
87884
|
apple: () => {
|
|
@@ -87686,6 +87892,8 @@ Do not use when: you don't yet have an id (use search-messages or list-messages
|
|
|
87686
87892
|
}
|
|
87687
87893
|
const isHtml = preferHtml === true && !!content.htmlContent;
|
|
87688
87894
|
const body = isHtml ? content.htmlContent : content.plainText;
|
|
87895
|
+
const dateSent = isoOrUndefined(content.dateSent);
|
|
87896
|
+
const dateReceived = isoOrUndefined(content.dateReceived);
|
|
87689
87897
|
return successResponse(`Subject: ${content.subject}
|
|
87690
87898
|
|
|
87691
87899
|
${body}`, {
|
|
@@ -87693,7 +87901,9 @@ ${body}`, {
|
|
|
87693
87901
|
subject: content.subject,
|
|
87694
87902
|
body,
|
|
87695
87903
|
isHtml,
|
|
87696
|
-
rfcMessageId: content.rfcMessageId ?? ""
|
|
87904
|
+
rfcMessageId: content.rfcMessageId ?? "",
|
|
87905
|
+
...dateSent ? { dateSent } : {},
|
|
87906
|
+
...dateReceived ? { dateReceived } : {}
|
|
87697
87907
|
});
|
|
87698
87908
|
},
|
|
87699
87909
|
ok: "",
|
|
@@ -87702,6 +87912,63 @@ ${body}`, {
|
|
|
87702
87912
|
"Error retrieving message"
|
|
87703
87913
|
)
|
|
87704
87914
|
);
|
|
87915
|
+
var HEADER_FIELD_SCHEMA = external_exports.object({
|
|
87916
|
+
name: external_exports.string().describe("Header name as written (case preserved)"),
|
|
87917
|
+
value: external_exports.string().describe("Unfolded value; RFC 2047 encoded-words left as-is")
|
|
87918
|
+
});
|
|
87919
|
+
registerTool(
|
|
87920
|
+
"get-message-headers",
|
|
87921
|
+
{
|
|
87922
|
+
description: "Use when: you need a message's raw RFC 5322 headers \u2014 the author's Date: header (not the mailbox arrival time), Message-ID, In-Reply-To/References, the Received: hop trace, or any custom X- header \u2014 for a message whose id you already have (numeric or imap:\u2026). Cheap: never downloads the body or attachments.\nReturns: the raw header block (text), every header as ordered {name, value} pairs with folding undone, and the decoded key fields: date (ISO 8601, from the Date: header), dateHeader (verbatim), dateReceived (mailbox arrival time \u2014 the value a migration or re-import resets, so compare it with date), messageId, subject, from, to, cc, replyTo, inReplyTo, references[], received[].\nTip: pass the mailbox+account you got the id from so a numeric id is fetched directly instead of scanning every mailbox.\nDo not use when: you want the body (use get-message), the conversation (use get-thread), or only the Message-ID (get-message already returns rfcMessageId).",
|
|
87923
|
+
inputSchema: {
|
|
87924
|
+
id: MESSAGE_ID_SCHEMA,
|
|
87925
|
+
mailbox: external_exports.string().optional().describe(
|
|
87926
|
+
"Mailbox that holds the message (numeric ids are unique per mailbox). With `account`, opens that mailbox directly instead of scanning every mailbox."
|
|
87927
|
+
),
|
|
87928
|
+
account: external_exports.string().optional().describe("Account that holds the message. Pair with `mailbox` for a direct fetch.")
|
|
87929
|
+
},
|
|
87930
|
+
outputSchema: {
|
|
87931
|
+
id: external_exports.string().optional(),
|
|
87932
|
+
raw: external_exports.string().optional().describe("The raw header block, exactly as stored"),
|
|
87933
|
+
headers: external_exports.array(HEADER_FIELD_SCHEMA).optional(),
|
|
87934
|
+
headerCount: external_exports.number().optional(),
|
|
87935
|
+
date: external_exports.string().optional().describe("ISO 8601 from the Date: header \u2014 the author's send time"),
|
|
87936
|
+
dateHeader: external_exports.string().optional().describe("The Date: header verbatim"),
|
|
87937
|
+
dateReceived: external_exports.string().optional().describe("ISO 8601 mailbox arrival time (INTERNALDATE / Mail's `date received`)"),
|
|
87938
|
+
messageId: external_exports.string().optional().describe("Bare RFC 5322 Message-ID"),
|
|
87939
|
+
subject: external_exports.string().optional().describe("RFC 2047-decoded Subject:"),
|
|
87940
|
+
from: external_exports.string().optional(),
|
|
87941
|
+
to: external_exports.string().optional(),
|
|
87942
|
+
cc: external_exports.string().optional(),
|
|
87943
|
+
replyTo: external_exports.string().optional(),
|
|
87944
|
+
inReplyTo: external_exports.string().optional(),
|
|
87945
|
+
references: external_exports.array(external_exports.string()).optional(),
|
|
87946
|
+
received: external_exports.array(external_exports.string()).optional().describe("Every Received: header, as written (first = last hop)")
|
|
87947
|
+
},
|
|
87948
|
+
annotations: { readOnlyHint: true }
|
|
87949
|
+
},
|
|
87950
|
+
withErrorHandling(
|
|
87951
|
+
({ id, mailbox, account }) => routeMessage(id, {
|
|
87952
|
+
// imap: id → BODY.PEEK[HEADER] + INTERNALDATE, no body download.
|
|
87953
|
+
imap: () => imapGetMessageHeaders(id, { account }),
|
|
87954
|
+
structuredFromResult: (r) => r.info ? headersStructured(id, parseHeaderBlock(r.info), r.meta?.dateReceived) : void 0,
|
|
87955
|
+
apple: () => {
|
|
87956
|
+
const h = mailManager.getMessageHeaders(id, { account, mailbox });
|
|
87957
|
+
if (!h) {
|
|
87958
|
+
const lookupError = mailManager.consumeLastMessageLookupError();
|
|
87959
|
+
return errorResponse(lookupError ?? `Message with ID "${id}" not found`);
|
|
87960
|
+
}
|
|
87961
|
+
return successResponse(
|
|
87962
|
+
h.raw,
|
|
87963
|
+
headersStructured(id, parseHeaderBlock(h.raw), h.dateReceived)
|
|
87964
|
+
);
|
|
87965
|
+
},
|
|
87966
|
+
ok: "",
|
|
87967
|
+
fail: `Message with ID "${id}" not found`
|
|
87968
|
+
}),
|
|
87969
|
+
"Error retrieving message headers"
|
|
87970
|
+
)
|
|
87971
|
+
);
|
|
87705
87972
|
registerTool(
|
|
87706
87973
|
"get-thread",
|
|
87707
87974
|
{
|
package/docs/IMAP-SETUP.md
CHANGED
|
@@ -28,7 +28,7 @@ When an account is IMAP-configured, these route to IMAP (otherwise AppleScript):
|
|
|
28
28
|
| Capability | Tools |
|
|
29
29
|
|------------|-------|
|
|
30
30
|
| Server-side search / list | `search-messages`, `list-messages` |
|
|
31
|
-
| Read a message | `get-message` |
|
|
31
|
+
| Read a message | `get-message`, `get-message-headers` |
|
|
32
32
|
| Message mutations | `mark-as-read`/`unread`, `flag`/`unflag-message`, `move-message`, `delete-message` |
|
|
33
33
|
| Batch mutations | `batch-mark-as-read`/`unread`, `batch-flag`/`unflag-messages`, `batch-move-messages`, `batch-delete-messages` |
|
|
34
34
|
| Folder ops | `create-mailbox`, `rename-mailbox`, `delete-mailbox` |
|
package/package.json
CHANGED