apple-mail-mcp 2.8.8 → 2.8.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 +9 -4
- package/build/cli.js +40 -4
- package/build/index.js +367 -155
- package/docs/IMAP-SETUP.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -291,8 +291,8 @@ Send a new email immediately.
|
|
|
291
291
|
| `body` | string | Yes | Email body (plain text) |
|
|
292
292
|
| `cc` | string[] | No | CC recipients |
|
|
293
293
|
| `bcc` | string[] | No | BCC recipients |
|
|
294
|
-
| `account` | string | No |
|
|
295
|
-
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths (e.g., `"/Users/me/report.pdf"`) and/or inline `{filename, contentBase64}` objects
|
|
294
|
+
| `account` | string | No | Mail.app account label, or an email-form SMTP From override. An SMTP override must match `APPLE_MAIL_MCP_SMTP_USER`, `APPLE_MAIL_MCP_SMTP_FROM`, or an address in `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` |
|
|
295
|
+
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths (e.g., `"/Users/me/report.pdf"`) and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
|
|
296
296
|
| `transport` | `"applescript"` \| `"smtp"` | No | Send transport. If omitted, **SMTP is used automatically when configured** (otherwise AppleScript). Pass `"smtp"` to require clean MIME, or `"applescript"` to force the Mail.app path — see [SMTP transport](#smtp-transport) |
|
|
297
297
|
|
|
298
298
|
**Example:**
|
|
@@ -326,7 +326,11 @@ Two differences to know when SMTP is auto-preferred:
|
|
|
326
326
|
is used as the From address only when it is an email address; a Mail.app
|
|
327
327
|
account *label* (e.g. `"Work"`) can't select an account over SMTP, so a call
|
|
328
328
|
that passes one is left on the AppleScript path automatically. To force
|
|
329
|
-
account selection, pass `transport: "applescript"` explicitly.
|
|
329
|
+
account selection, pass `transport: "applescript"` explicitly. For sender
|
|
330
|
+
safety, an email-form override must match the SMTP login user, the configured
|
|
331
|
+
`APPLE_MAIL_MCP_SMTP_FROM`, or an address listed in the comma-separated
|
|
332
|
+
`APPLE_MAIL_MCP_SMTP_ALLOWED_FROM`; any other From address is rejected before
|
|
333
|
+
connecting.
|
|
330
334
|
|
|
331
335
|
Both plain-text and HTML bodies are supported — over SMTP an HTML body (CLI
|
|
332
336
|
`--html-body-file`) is sent as `multipart/alternative` with the plain-text
|
|
@@ -342,6 +346,7 @@ read from the macOS **Keychain** by default, so no secret goes in config:
|
|
|
342
346
|
| `APPLE_MAIL_MCP_SMTP_PORT` | No | `465` if secure, else `587` | SMTP port |
|
|
343
347
|
| `APPLE_MAIL_MCP_SMTP_SECURE` | No | `false` | `true` for implicit TLS (port 465); otherwise STARTTLS |
|
|
344
348
|
| `APPLE_MAIL_MCP_SMTP_FROM` | No | = user | From address |
|
|
349
|
+
| `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` | No | — | Comma-separated sender aliases permitted as per-message From overrides |
|
|
345
350
|
| `APPLE_MAIL_MCP_SMTP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
|
|
346
351
|
| `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` | No | = host | Keychain item service/server name |
|
|
347
352
|
| `APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
|
|
@@ -625,7 +630,7 @@ Save an email to Drafts without sending.
|
|
|
625
630
|
| `cc` | string[] | No | CC recipients |
|
|
626
631
|
| `bcc` | string[] | No | BCC recipients |
|
|
627
632
|
| `account` | string | No | Account for draft |
|
|
628
|
-
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths and/or inline `{filename, contentBase64}` objects |
|
|
633
|
+
| `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
|
|
629
634
|
|
|
630
635
|
**Returns:** Confirmation that draft was created.
|
|
631
636
|
|
package/build/cli.js
CHANGED
|
@@ -11869,6 +11869,29 @@ import { execFileSync } from "child_process";
|
|
|
11869
11869
|
import { isAbsolute } from "path";
|
|
11870
11870
|
import { existsSync } from "fs";
|
|
11871
11871
|
|
|
11872
|
+
// src/utils/attachmentLimits.ts
|
|
11873
|
+
var MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
11874
|
+
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = Math.ceil(MAX_INLINE_ATTACHMENT_BYTES / 3) * 4;
|
|
11875
|
+
var MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS = MAX_INLINE_ATTACHMENT_BASE64_CHARS * 2;
|
|
11876
|
+
function isInlineAttachmentBase64WithinLimit(contentBase64) {
|
|
11877
|
+
if (contentBase64.length > MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS) return false;
|
|
11878
|
+
let encodedChars = 0;
|
|
11879
|
+
for (const char of contentBase64) {
|
|
11880
|
+
if (!/\s/u.test(char) && ++encodedChars > MAX_INLINE_ATTACHMENT_BASE64_CHARS) return false;
|
|
11881
|
+
}
|
|
11882
|
+
return true;
|
|
11883
|
+
}
|
|
11884
|
+
function decodeInlineAttachment(contentBase64) {
|
|
11885
|
+
if (!isInlineAttachmentBase64WithinLimit(contentBase64)) {
|
|
11886
|
+
throw new Error("Inline attachment exceeds the 25 MiB decoded size limit.");
|
|
11887
|
+
}
|
|
11888
|
+
const content = Buffer.from(contentBase64, "base64");
|
|
11889
|
+
if (content.length > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
11890
|
+
throw new Error("Inline attachment exceeds the 25 MiB decoded size limit.");
|
|
11891
|
+
}
|
|
11892
|
+
return content;
|
|
11893
|
+
}
|
|
11894
|
+
|
|
11872
11895
|
// src/utils/docsUrls.ts
|
|
11873
11896
|
var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
|
|
11874
11897
|
var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
|
|
@@ -11880,6 +11903,7 @@ var SMTP_ENV = {
|
|
|
11880
11903
|
secure: "APPLE_MAIL_MCP_SMTP_SECURE",
|
|
11881
11904
|
user: "APPLE_MAIL_MCP_SMTP_USER",
|
|
11882
11905
|
from: "APPLE_MAIL_MCP_SMTP_FROM",
|
|
11906
|
+
allowedFrom: "APPLE_MAIL_MCP_SMTP_ALLOWED_FROM",
|
|
11883
11907
|
password: "APPLE_MAIL_MCP_SMTP_PASSWORD",
|
|
11884
11908
|
keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE",
|
|
11885
11909
|
keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT"
|
|
@@ -11915,6 +11939,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
11915
11939
|
throw new Error(`Invalid ${SMTP_ENV.port}: "${env[SMTP_ENV.port]}" is not a valid port.`);
|
|
11916
11940
|
}
|
|
11917
11941
|
const from = env[SMTP_ENV.from]?.trim() || user;
|
|
11942
|
+
const allowedFrom = (env[SMTP_ENV.allowedFrom] ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
11918
11943
|
let pass = env[SMTP_ENV.password];
|
|
11919
11944
|
if (!pass) {
|
|
11920
11945
|
const service = env[SMTP_ENV.keychainService]?.trim() || host;
|
|
@@ -11926,7 +11951,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
11926
11951
|
`No SMTP password found. Set ${SMTP_ENV.password}, or store an internet password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}". ` + SETUP_HINT
|
|
11927
11952
|
);
|
|
11928
11953
|
}
|
|
11929
|
-
return { host, port, secure, user, pass, from };
|
|
11954
|
+
return { host, port, secure, user, pass, from, allowedFrom };
|
|
11930
11955
|
}
|
|
11931
11956
|
function buildAttachments(attachments) {
|
|
11932
11957
|
if (!attachments || attachments.length === 0) return void 0;
|
|
@@ -11939,7 +11964,7 @@ function buildAttachments(attachments) {
|
|
|
11939
11964
|
if (!a.filename || !a.contentBase64) {
|
|
11940
11965
|
throw new Error("Inline attachment requires both filename and contentBase64.");
|
|
11941
11966
|
}
|
|
11942
|
-
return { filename: a.filename, content:
|
|
11967
|
+
return { filename: a.filename, content: decodeInlineAttachment(a.contentBase64) };
|
|
11943
11968
|
});
|
|
11944
11969
|
}
|
|
11945
11970
|
async function sendViaSmtp(opts, config, createTransport = import_nodemailer.default.createTransport) {
|
|
@@ -11949,6 +11974,16 @@ async function sendViaSmtp(opts, config, createTransport = import_nodemailer.def
|
|
|
11949
11974
|
} catch (error) {
|
|
11950
11975
|
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
11951
11976
|
}
|
|
11977
|
+
const requestedFrom = opts.from?.trim();
|
|
11978
|
+
const allowedFrom = new Set(
|
|
11979
|
+
[cfg.user, cfg.from, ...cfg.allowedFrom ?? []].map((value) => value.trim().toLowerCase())
|
|
11980
|
+
);
|
|
11981
|
+
if (requestedFrom && !allowedFrom.has(requestedFrom.toLowerCase())) {
|
|
11982
|
+
return {
|
|
11983
|
+
success: false,
|
|
11984
|
+
error: `SMTP From "${requestedFrom}" is not a configured sender identity.`
|
|
11985
|
+
};
|
|
11986
|
+
}
|
|
11952
11987
|
let attachments;
|
|
11953
11988
|
try {
|
|
11954
11989
|
attachments = buildAttachments(opts.attachments);
|
|
@@ -11964,7 +11999,7 @@ async function sendViaSmtp(opts, config, createTransport = import_nodemailer.def
|
|
|
11964
11999
|
const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
|
|
11965
12000
|
try {
|
|
11966
12001
|
const info = await transporter.sendMail({
|
|
11967
|
-
from:
|
|
12002
|
+
from: requestedFrom || cfg.from,
|
|
11968
12003
|
to: opts.to,
|
|
11969
12004
|
cc: opts.cc,
|
|
11970
12005
|
bcc: opts.bcc,
|
|
@@ -11995,7 +12030,8 @@ var EX_CONFIG = 78;
|
|
|
11995
12030
|
var USAGE = `apple-mail-send \u2014 send a clean email via SMTP (no Mail.app blockquote wrapping).
|
|
11996
12031
|
|
|
11997
12032
|
Required:
|
|
11998
|
-
--from <addr> Sender address (
|
|
12033
|
+
--from <addr> Sender address (SMTP user/configured From, or an alias in
|
|
12034
|
+
${SMTP_ENV.allowedFrom})
|
|
11999
12035
|
--to <addr> Recipient (repeatable)
|
|
12000
12036
|
--subject <text> Subject line
|
|
12001
12037
|
--body-file <path> UTF-8 file with the plain-text body
|
package/build/index.js
CHANGED
|
@@ -75678,7 +75678,15 @@ var StdioServerTransport = class {
|
|
|
75678
75678
|
|
|
75679
75679
|
// src/services/appleMailManager.ts
|
|
75680
75680
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
75681
|
-
import {
|
|
75681
|
+
import {
|
|
75682
|
+
existsSync as existsSync3,
|
|
75683
|
+
writeFileSync as writeFileSync3,
|
|
75684
|
+
readFileSync as readFileSync2,
|
|
75685
|
+
mkdtempSync as mkdtempSync2,
|
|
75686
|
+
rmSync as rmSync2,
|
|
75687
|
+
realpathSync,
|
|
75688
|
+
lstatSync
|
|
75689
|
+
} from "fs";
|
|
75682
75690
|
import { isAbsolute, resolve, sep, join as join4 } from "path";
|
|
75683
75691
|
import { homedir as homedir3 } from "os";
|
|
75684
75692
|
|
|
@@ -76095,6 +76103,14 @@ function extractTextBody(source) {
|
|
|
76095
76103
|
const encoding = getHeader(headers, "Content-Transfer-Encoding");
|
|
76096
76104
|
return decodeBody(body, encoding).toString("utf8");
|
|
76097
76105
|
}
|
|
76106
|
+
function extractRfcMessageIdFromSource(source) {
|
|
76107
|
+
if (!source || !source.trim()) return "";
|
|
76108
|
+
const blankLineIdx = source.search(/\r?\n\r?\n/);
|
|
76109
|
+
const headers = blankLineIdx === -1 ? source : source.substring(0, blankLineIdx);
|
|
76110
|
+
const raw = getHeader(headers, "Message-ID") ?? getHeader(headers, "Message-Id");
|
|
76111
|
+
if (!raw) return "";
|
|
76112
|
+
return raw.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
76113
|
+
}
|
|
76098
76114
|
function extractMimeAttachment(source, attachmentName) {
|
|
76099
76115
|
if (!source || !source.trim()) return null;
|
|
76100
76116
|
const boundary = extractBoundary(source);
|
|
@@ -76192,22 +76208,53 @@ var TemplateStore = class {
|
|
|
76192
76208
|
import { writeFileSync as writeFileSync2, rmSync, mkdtempSync } from "fs";
|
|
76193
76209
|
import { join as join2 } from "path";
|
|
76194
76210
|
import { tmpdir } from "os";
|
|
76211
|
+
|
|
76212
|
+
// src/utils/attachmentLimits.ts
|
|
76213
|
+
var MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
76214
|
+
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = Math.ceil(MAX_INLINE_ATTACHMENT_BYTES / 3) * 4;
|
|
76215
|
+
var MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS = MAX_INLINE_ATTACHMENT_BASE64_CHARS * 2;
|
|
76216
|
+
function isInlineAttachmentBase64WithinLimit(contentBase64) {
|
|
76217
|
+
if (contentBase64.length > MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS) return false;
|
|
76218
|
+
let encodedChars = 0;
|
|
76219
|
+
for (const char of contentBase64) {
|
|
76220
|
+
if (!/\s/u.test(char) && ++encodedChars > MAX_INLINE_ATTACHMENT_BASE64_CHARS) return false;
|
|
76221
|
+
}
|
|
76222
|
+
return true;
|
|
76223
|
+
}
|
|
76224
|
+
function decodeInlineAttachment(contentBase64) {
|
|
76225
|
+
if (!isInlineAttachmentBase64WithinLimit(contentBase64)) {
|
|
76226
|
+
throw new Error("Inline attachment exceeds the 25 MiB decoded size limit.");
|
|
76227
|
+
}
|
|
76228
|
+
const content = Buffer.from(contentBase64, "base64");
|
|
76229
|
+
if (content.length > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
76230
|
+
throw new Error("Inline attachment exceeds the 25 MiB decoded size limit.");
|
|
76231
|
+
}
|
|
76232
|
+
return content;
|
|
76233
|
+
}
|
|
76234
|
+
|
|
76235
|
+
// src/utils/attachmentMaterialize.ts
|
|
76195
76236
|
function materializeAttachments(attachments) {
|
|
76196
76237
|
if (!attachments || attachments.length === 0) {
|
|
76197
76238
|
return { paths: [], cleanup: () => void 0 };
|
|
76198
76239
|
}
|
|
76199
76240
|
let dir = null;
|
|
76200
|
-
|
|
76201
|
-
|
|
76202
|
-
|
|
76203
|
-
|
|
76204
|
-
|
|
76205
|
-
|
|
76206
|
-
|
|
76207
|
-
|
|
76208
|
-
|
|
76209
|
-
|
|
76210
|
-
|
|
76241
|
+
let paths;
|
|
76242
|
+
try {
|
|
76243
|
+
paths = attachments.map((a) => {
|
|
76244
|
+
if (typeof a === "string") return a;
|
|
76245
|
+
if (!a.filename || !a.contentBase64) {
|
|
76246
|
+
throw new Error("Inline attachment requires both filename and contentBase64.");
|
|
76247
|
+
}
|
|
76248
|
+
if (!dir) dir = mkdtempSync(join2(tmpdir(), "amcp-att-"));
|
|
76249
|
+
const safeName = a.filename.replace(/[/\\]/g, "_");
|
|
76250
|
+
const p = join2(dir, safeName);
|
|
76251
|
+
writeFileSync2(p, decodeInlineAttachment(a.contentBase64));
|
|
76252
|
+
return p;
|
|
76253
|
+
});
|
|
76254
|
+
} catch (error2) {
|
|
76255
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
76256
|
+
throw error2;
|
|
76257
|
+
}
|
|
76211
76258
|
return {
|
|
76212
76259
|
paths,
|
|
76213
76260
|
cleanup: () => {
|
|
@@ -76341,8 +76388,12 @@ var DIAG_MARKER = "DIAG";
|
|
|
76341
76388
|
var DIAG_FIELD_SEP = "F";
|
|
76342
76389
|
var DIAG_ITEM_SEP = "M";
|
|
76343
76390
|
var CONTENT_MARKER = "CONTENT";
|
|
76391
|
+
var MSGID_MARKER = "MSGID";
|
|
76344
76392
|
var HTML_MARKER = "HTML";
|
|
76345
76393
|
var BATCH_FATAL = "FATAL";
|
|
76394
|
+
function normalizeRfcMessageId(mid) {
|
|
76395
|
+
return (mid || "").trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
76396
|
+
}
|
|
76346
76397
|
function mergeSearchDiagnostics(into, from) {
|
|
76347
76398
|
into.timedOutAccounts.push(...from.timedOutAccounts);
|
|
76348
76399
|
into.skippedLargeMailboxes.push(...from.skippedLargeMailboxes);
|
|
@@ -76386,6 +76437,25 @@ function isPathWithinAllowedRoots(resolvedPath) {
|
|
|
76386
76437
|
return resolvedPath === base || resolvedPath.startsWith(base + sep);
|
|
76387
76438
|
});
|
|
76388
76439
|
}
|
|
76440
|
+
function resolveAttachmentSaveTarget(savePath, attachmentName) {
|
|
76441
|
+
let saveDirectory;
|
|
76442
|
+
try {
|
|
76443
|
+
saveDirectory = realpathSync(resolve(savePath));
|
|
76444
|
+
} catch {
|
|
76445
|
+
throw new Error(`Save directory "${savePath}" does not exist`);
|
|
76446
|
+
}
|
|
76447
|
+
if (!isPathWithinAllowedRoots(saveDirectory)) {
|
|
76448
|
+
throw new Error(`Save path "${savePath}" is outside allowed directories`);
|
|
76449
|
+
}
|
|
76450
|
+
const savedPath = resolve(saveDirectory, attachmentName);
|
|
76451
|
+
if (!isPathWithinAllowedRoots(savedPath)) {
|
|
76452
|
+
throw new Error(`Output path "${savedPath}" is outside allowed directories`);
|
|
76453
|
+
}
|
|
76454
|
+
if (existsSync3(savedPath) && lstatSync(savedPath).isSymbolicLink()) {
|
|
76455
|
+
throw new Error(`Refusing to overwrite symbolic link "${savedPath}"`);
|
|
76456
|
+
}
|
|
76457
|
+
return { saveDirectory, savedPath };
|
|
76458
|
+
}
|
|
76389
76459
|
var UNSUPPORTED_APPLESCRIPT_OP = /AppleEvent handler failed|-10000/i;
|
|
76390
76460
|
function describeMailboxOpError(op, raw) {
|
|
76391
76461
|
const trimmed = (raw || "").trim();
|
|
@@ -76621,6 +76691,32 @@ var AppleMailManager = class {
|
|
|
76621
76691
|
};
|
|
76622
76692
|
/** Cache TTL in milliseconds (60 seconds). */
|
|
76623
76693
|
CACHE_TTL_MS = 6e4;
|
|
76694
|
+
/**
|
|
76695
|
+
* Remembers where each message id was last seen: id → {account, mailbox}.
|
|
76696
|
+
*
|
|
76697
|
+
* Mail.app numeric message ids are unique *per mailbox*, and by-id fetches
|
|
76698
|
+
* (getMessageContent/getRawSource) otherwise have to linear-scan every mailbox
|
|
76699
|
+
* of every account probing `whose id is N`. On a real multi-account setup that
|
|
76700
|
+
* is 700+ mailboxes; a message in a late-iterated folder (e.g. a large "Sent
|
|
76701
|
+
* Items") isn't reached before the AppleScript timeout fires, so the fetch
|
|
76702
|
+
* returns a false "not found" (only INBOX ids, reached early, worked). Every
|
|
76703
|
+
* search/list/by-id result records its id→location here so a subsequent fetch
|
|
76704
|
+
* opens the one right mailbox directly. A stale entry (message moved) simply
|
|
76705
|
+
* misses and falls back to the full scan, so it can never wedge a lookup.
|
|
76706
|
+
*/
|
|
76707
|
+
idLocationIndex = /* @__PURE__ */ new Map();
|
|
76708
|
+
/** Cap on the id→location index so a long-lived process can't grow unbounded. */
|
|
76709
|
+
ID_LOCATION_MAX = 5e3;
|
|
76710
|
+
/** Record (or refresh) where a message id lives, evicting oldest when full. */
|
|
76711
|
+
rememberLocation(id, account, mailbox) {
|
|
76712
|
+
if (!id || !account || !mailbox) return;
|
|
76713
|
+
if (this.idLocationIndex.has(id)) this.idLocationIndex.delete(id);
|
|
76714
|
+
this.idLocationIndex.set(id, { account, mailbox });
|
|
76715
|
+
if (this.idLocationIndex.size > this.ID_LOCATION_MAX) {
|
|
76716
|
+
const oldest = this.idLocationIndex.keys().next().value;
|
|
76717
|
+
if (oldest !== void 0) this.idLocationIndex.delete(oldest);
|
|
76718
|
+
}
|
|
76719
|
+
}
|
|
76624
76720
|
/**
|
|
76625
76721
|
* Returns cached accounts or fetches fresh data if cache is expired/empty.
|
|
76626
76722
|
*/
|
|
@@ -77123,6 +77219,7 @@ var AppleMailManager = class {
|
|
|
77123
77219
|
}
|
|
77124
77220
|
const parts = result.output.split(FIELD_SEP);
|
|
77125
77221
|
if (parts.length < 9) return null;
|
|
77222
|
+
this.rememberLocation(id.toString(), parts[8], parts[7]);
|
|
77126
77223
|
return {
|
|
77127
77224
|
id: id.toString(),
|
|
77128
77225
|
subject: parts[0],
|
|
@@ -77138,6 +77235,47 @@ var AppleMailManager = class {
|
|
|
77138
77235
|
hasAttachments: parts.length > 9 ? parts[9] === "true" : false
|
|
77139
77236
|
};
|
|
77140
77237
|
}
|
|
77238
|
+
/**
|
|
77239
|
+
* Build an app-level AppleScript that opens exactly one account+mailbox, finds
|
|
77240
|
+
* the message with numeric `id` in it, and runs `innerAction` (which may assume
|
|
77241
|
+
* `msg` is bound). Used by the by-id fast paths (getMessageContent/getRawSource)
|
|
77242
|
+
* so a message in a late-iterated large folder resolves directly instead of via
|
|
77243
|
+
* the timeout-prone full-mailbox scan.
|
|
77244
|
+
*
|
|
77245
|
+
* The mailbox name is resolved through `resolveMailbox` (so an alias like
|
|
77246
|
+
* "Sent"→"Sent Items" or a casing mismatch like "INBOX"→"Inbox" still opens the
|
|
77247
|
+
* right folder), and matched case-insensitively by iterating the account's
|
|
77248
|
+
* mailboxes — `mailbox "INBOX" of account …` throws on accounts whose inbox is
|
|
77249
|
+
* actually named "Inbox", which would silently drop us back to the slow scan.
|
|
77250
|
+
* Returns "" (found nothing) on any error, so the caller falls back safely.
|
|
77251
|
+
*/
|
|
77252
|
+
scopedByIdScript(account, mailbox, id, innerAction) {
|
|
77253
|
+
const resolved = this.resolveMailbox(mailbox, account);
|
|
77254
|
+
return buildAppLevelScript(`
|
|
77255
|
+
try
|
|
77256
|
+
set acct to (first account whose name is "${escapeForAppleScript(account)}")
|
|
77257
|
+
set targetMb to missing value
|
|
77258
|
+
ignoring case
|
|
77259
|
+
repeat with mb in mailboxes of acct
|
|
77260
|
+
if (name of mb) is "${escapeForAppleScript(resolved)}" then
|
|
77261
|
+
set targetMb to mb
|
|
77262
|
+
exit repeat
|
|
77263
|
+
end if
|
|
77264
|
+
end repeat
|
|
77265
|
+
end ignoring
|
|
77266
|
+
if targetMb is not missing value then
|
|
77267
|
+
set matchingMsgs to (messages of targetMb whose id is ${Number(id)})
|
|
77268
|
+
if (count of matchingMsgs) > 0 then
|
|
77269
|
+
set msg to item 1 of matchingMsgs
|
|
77270
|
+
${innerAction}
|
|
77271
|
+
end if
|
|
77272
|
+
end if
|
|
77273
|
+
return ""
|
|
77274
|
+
on error errMsg
|
|
77275
|
+
return ""
|
|
77276
|
+
end try
|
|
77277
|
+
`);
|
|
77278
|
+
}
|
|
77141
77279
|
/**
|
|
77142
77280
|
* Get the content of a message.
|
|
77143
77281
|
*
|
|
@@ -77148,11 +77286,30 @@ var AppleMailManager = class {
|
|
|
77148
77286
|
* path doesn't need it; fetching it unconditionally was both slow and, worse,
|
|
77149
77287
|
* returned the entire raw MIME blob mislabeled as HTML (#32).
|
|
77150
77288
|
*/
|
|
77151
|
-
getMessageContent(id, includeHtml = false) {
|
|
77289
|
+
getMessageContent(id, includeHtml = false, hint) {
|
|
77152
77290
|
const sourceFetch = includeHtml ? `set htmlSource to ""
|
|
77153
77291
|
try
|
|
77154
77292
|
set htmlSource to source of msg
|
|
77155
77293
|
end try` : `set htmlSource to ""`;
|
|
77294
|
+
const innerFetch = `
|
|
77295
|
+
set msgSubject to subject of msg
|
|
77296
|
+
set msgRfcId to ""
|
|
77297
|
+
try
|
|
77298
|
+
set msgRfcId to message id of msg
|
|
77299
|
+
end try
|
|
77300
|
+
set msgContent to content of msg
|
|
77301
|
+
${sourceFetch}
|
|
77302
|
+
return msgSubject & "${MSGID_MARKER}" & msgRfcId & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlSource`;
|
|
77303
|
+
const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
|
|
77304
|
+
if (loc) {
|
|
77305
|
+
const scopedScript = this.scopedByIdScript(loc.account, loc.mailbox, id, innerFetch);
|
|
77306
|
+
const scoped = this.parseMessageContent(
|
|
77307
|
+
id,
|
|
77308
|
+
executeAppleScript(scopedScript, { timeoutMs: 6e4 }),
|
|
77309
|
+
includeHtml
|
|
77310
|
+
);
|
|
77311
|
+
if (scoped) return scoped;
|
|
77312
|
+
}
|
|
77156
77313
|
const script = buildAppLevelScript(`
|
|
77157
77314
|
try
|
|
77158
77315
|
repeat with acct in accounts
|
|
@@ -77161,10 +77318,7 @@ var AppleMailManager = class {
|
|
|
77161
77318
|
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
77162
77319
|
if (count of matchingMsgs) > 0 then
|
|
77163
77320
|
set msg to item 1 of matchingMsgs
|
|
77164
|
-
|
|
77165
|
-
set msgContent to content of msg
|
|
77166
|
-
${sourceFetch}
|
|
77167
|
-
return msgSubject & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlSource
|
|
77321
|
+
${innerFetch}
|
|
77168
77322
|
end if
|
|
77169
77323
|
end try
|
|
77170
77324
|
end repeat
|
|
@@ -77174,9 +77328,20 @@ var AppleMailManager = class {
|
|
|
77174
77328
|
return ""
|
|
77175
77329
|
end try
|
|
77176
77330
|
`);
|
|
77177
|
-
|
|
77331
|
+
return this.parseMessageContent(
|
|
77332
|
+
id,
|
|
77333
|
+
executeAppleScript(script, { timeoutMs: 6e4 }),
|
|
77334
|
+
includeHtml
|
|
77335
|
+
);
|
|
77336
|
+
}
|
|
77337
|
+
/**
|
|
77338
|
+
* Parse the marker-delimited output of a getMessageContent AppleScript into a
|
|
77339
|
+
* MessageContent, or null when nothing was found / the fetch failed. Shared by
|
|
77340
|
+
* the scoped fast path and the full-mailbox-scan fallback.
|
|
77341
|
+
*/
|
|
77342
|
+
parseMessageContent(id, result, includeHtml) {
|
|
77178
77343
|
if (!result.success || !result.output.trim()) {
|
|
77179
|
-
console.error(`Failed to get message content: ${result.error}`);
|
|
77344
|
+
if (!result.success) console.error(`Failed to get message content: ${result.error}`);
|
|
77180
77345
|
return null;
|
|
77181
77346
|
}
|
|
77182
77347
|
const htmlSplit = result.output.split(HTML_MARKER);
|
|
@@ -77184,12 +77349,16 @@ var AppleMailManager = class {
|
|
|
77184
77349
|
const rawSource = htmlSplit.length > 1 ? htmlSplit[1] : "";
|
|
77185
77350
|
const parts = contentPart.split(CONTENT_MARKER);
|
|
77186
77351
|
if (parts.length < 2) return null;
|
|
77352
|
+
const subjParts = parts[0].split(MSGID_MARKER);
|
|
77353
|
+
const subject = subjParts[0];
|
|
77354
|
+
const rfcMessageId = normalizeRfcMessageId(subjParts.length > 1 ? subjParts[1] : "");
|
|
77187
77355
|
const htmlContent = includeHtml && rawSource ? extractHtmlBody(rawSource) || void 0 : void 0;
|
|
77188
77356
|
return {
|
|
77189
77357
|
id: id.toString(),
|
|
77190
|
-
subject
|
|
77358
|
+
subject,
|
|
77191
77359
|
plainText: parts[1],
|
|
77192
|
-
htmlContent
|
|
77360
|
+
htmlContent,
|
|
77361
|
+
rfcMessageId
|
|
77193
77362
|
};
|
|
77194
77363
|
}
|
|
77195
77364
|
/**
|
|
@@ -77201,7 +77370,18 @@ var AppleMailManager = class {
|
|
|
77201
77370
|
* the entire raw message including base64-encoded attachments —
|
|
77202
77371
|
* a 20MB attachment can take several seconds over Exchange/IMAP.
|
|
77203
77372
|
*/
|
|
77204
|
-
getRawSource(id) {
|
|
77373
|
+
getRawSource(id, hint) {
|
|
77374
|
+
const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
|
|
77375
|
+
if (loc) {
|
|
77376
|
+
const scopedScript = this.scopedByIdScript(
|
|
77377
|
+
loc.account,
|
|
77378
|
+
loc.mailbox,
|
|
77379
|
+
id,
|
|
77380
|
+
"return source of msg"
|
|
77381
|
+
);
|
|
77382
|
+
const scoped = executeAppleScript(scopedScript, { timeoutMs: 12e4 });
|
|
77383
|
+
if (scoped.success && scoped.output.trim()) return scoped.output;
|
|
77384
|
+
}
|
|
77205
77385
|
const script = buildAppLevelScript(`
|
|
77206
77386
|
try
|
|
77207
77387
|
repeat with acct in accounts
|
|
@@ -77372,8 +77552,9 @@ var AppleMailManager = class {
|
|
|
77372
77552
|
} else if (parts.length === 7) {
|
|
77373
77553
|
hasAttachments = parts[6] === "true";
|
|
77374
77554
|
}
|
|
77555
|
+
const msgId = parts[0].trim();
|
|
77375
77556
|
messages.push({
|
|
77376
|
-
id:
|
|
77557
|
+
id: msgId,
|
|
77377
77558
|
subject: parts[1],
|
|
77378
77559
|
sender: parts[2],
|
|
77379
77560
|
recipients: [],
|
|
@@ -77386,6 +77567,7 @@ var AppleMailManager = class {
|
|
|
77386
77567
|
account,
|
|
77387
77568
|
hasAttachments
|
|
77388
77569
|
});
|
|
77570
|
+
this.rememberLocation(msgId, account, msgMailbox);
|
|
77389
77571
|
}
|
|
77390
77572
|
return messages;
|
|
77391
77573
|
}
|
|
@@ -77793,7 +77975,7 @@ var AppleMailManager = class {
|
|
|
77793
77975
|
findNumericIdByMessageId(messageId, accountName) {
|
|
77794
77976
|
const mid = messageId.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
77795
77977
|
if (!mid) return null;
|
|
77796
|
-
const q = (s) => s
|
|
77978
|
+
const q = (s) => escapeForAppleScript(s);
|
|
77797
77979
|
const midLit = `"${q(mid)}"`;
|
|
77798
77980
|
const bracketedLit = `"${q(`<${mid}>`)}"`;
|
|
77799
77981
|
const matchClause = (mbVar) => `(messages of ${mbVar} whose message id is ${midLit} or message id is ${bracketedLit})`;
|
|
@@ -78180,13 +78362,15 @@ var AppleMailManager = class {
|
|
|
78180
78362
|
console.error(`Invalid attachment name: "${attachmentName}"`);
|
|
78181
78363
|
return false;
|
|
78182
78364
|
}
|
|
78183
|
-
|
|
78184
|
-
|
|
78185
|
-
|
|
78365
|
+
let target;
|
|
78366
|
+
try {
|
|
78367
|
+
target = resolveAttachmentSaveTarget(savePath, attachmentName);
|
|
78368
|
+
} catch (error2) {
|
|
78369
|
+
console.error(error2 instanceof Error ? error2.message : String(error2));
|
|
78186
78370
|
return false;
|
|
78187
78371
|
}
|
|
78188
78372
|
const safeName = escapeForAppleScript(attachmentName);
|
|
78189
|
-
const safePath = escapeForAppleScript(
|
|
78373
|
+
const safePath = escapeForAppleScript(target.saveDirectory);
|
|
78190
78374
|
const numericId = Number(id);
|
|
78191
78375
|
const script = buildAppLevelScript(`
|
|
78192
78376
|
try
|
|
@@ -78228,12 +78412,7 @@ var AppleMailManager = class {
|
|
|
78228
78412
|
return false;
|
|
78229
78413
|
}
|
|
78230
78414
|
try {
|
|
78231
|
-
|
|
78232
|
-
if (!isPathWithinAllowedRoots(outPath)) {
|
|
78233
|
-
console.error(`Output path "${outPath}" is outside allowed directories`);
|
|
78234
|
-
return false;
|
|
78235
|
-
}
|
|
78236
|
-
writeFileSync3(outPath, attachment.data);
|
|
78415
|
+
writeFileSync3(target.savedPath, attachment.data);
|
|
78237
78416
|
return true;
|
|
78238
78417
|
} catch (err) {
|
|
78239
78418
|
console.error(`Failed to write attachment to disk: ${err}`);
|
|
@@ -78250,7 +78429,7 @@ var AppleMailManager = class {
|
|
|
78250
78429
|
try {
|
|
78251
78430
|
dir = mkdtempSync2("/private/tmp/amcp-fetch-");
|
|
78252
78431
|
const dest = join4(dir, attachmentName.replace(/[/\\]/g, "_"));
|
|
78253
|
-
const ok = this.saveAttachment(id, attachmentName,
|
|
78432
|
+
const ok = this.saveAttachment(id, attachmentName, dir);
|
|
78254
78433
|
if (!ok) {
|
|
78255
78434
|
return {
|
|
78256
78435
|
success: false,
|
|
@@ -79001,7 +79180,7 @@ ${actionStmts.join("\n")}
|
|
|
79001
79180
|
|
|
79002
79181
|
// src/index.ts
|
|
79003
79182
|
import { writeFileSync as writeFileSync4 } from "fs";
|
|
79004
|
-
import {
|
|
79183
|
+
import { join as joinPath } from "path";
|
|
79005
79184
|
|
|
79006
79185
|
// src/services/smtpMailer.ts
|
|
79007
79186
|
var import_nodemailer = __toESM(require_nodemailer(), 1);
|
|
@@ -79020,6 +79199,7 @@ var SMTP_ENV = {
|
|
|
79020
79199
|
secure: "APPLE_MAIL_MCP_SMTP_SECURE",
|
|
79021
79200
|
user: "APPLE_MAIL_MCP_SMTP_USER",
|
|
79022
79201
|
from: "APPLE_MAIL_MCP_SMTP_FROM",
|
|
79202
|
+
allowedFrom: "APPLE_MAIL_MCP_SMTP_ALLOWED_FROM",
|
|
79023
79203
|
password: "APPLE_MAIL_MCP_SMTP_PASSWORD",
|
|
79024
79204
|
keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE",
|
|
79025
79205
|
keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT"
|
|
@@ -79065,6 +79245,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
79065
79245
|
throw new Error(`Invalid ${SMTP_ENV.port}: "${env[SMTP_ENV.port]}" is not a valid port.`);
|
|
79066
79246
|
}
|
|
79067
79247
|
const from = env[SMTP_ENV.from]?.trim() || user;
|
|
79248
|
+
const allowedFrom = (env[SMTP_ENV.allowedFrom] ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
79068
79249
|
let pass = env[SMTP_ENV.password];
|
|
79069
79250
|
if (!pass) {
|
|
79070
79251
|
const service = env[SMTP_ENV.keychainService]?.trim() || host;
|
|
@@ -79076,7 +79257,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
79076
79257
|
`No SMTP password found. Set ${SMTP_ENV.password}, or store an internet password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}". ` + SETUP_HINT
|
|
79077
79258
|
);
|
|
79078
79259
|
}
|
|
79079
|
-
return { host, port, secure, user, pass, from };
|
|
79260
|
+
return { host, port, secure, user, pass, from, allowedFrom };
|
|
79080
79261
|
}
|
|
79081
79262
|
function buildAttachments(attachments) {
|
|
79082
79263
|
if (!attachments || attachments.length === 0) return void 0;
|
|
@@ -79089,7 +79270,7 @@ function buildAttachments(attachments) {
|
|
|
79089
79270
|
if (!a.filename || !a.contentBase64) {
|
|
79090
79271
|
throw new Error("Inline attachment requires both filename and contentBase64.");
|
|
79091
79272
|
}
|
|
79092
|
-
return { filename: a.filename, content:
|
|
79273
|
+
return { filename: a.filename, content: decodeInlineAttachment(a.contentBase64) };
|
|
79093
79274
|
});
|
|
79094
79275
|
}
|
|
79095
79276
|
async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.default.createTransport) {
|
|
@@ -79099,6 +79280,16 @@ async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.de
|
|
|
79099
79280
|
} catch (error2) {
|
|
79100
79281
|
return { success: false, error: error2 instanceof Error ? error2.message : String(error2) };
|
|
79101
79282
|
}
|
|
79283
|
+
const requestedFrom = opts.from?.trim();
|
|
79284
|
+
const allowedFrom = new Set(
|
|
79285
|
+
[cfg.user, cfg.from, ...cfg.allowedFrom ?? []].map((value) => value.trim().toLowerCase())
|
|
79286
|
+
);
|
|
79287
|
+
if (requestedFrom && !allowedFrom.has(requestedFrom.toLowerCase())) {
|
|
79288
|
+
return {
|
|
79289
|
+
success: false,
|
|
79290
|
+
error: `SMTP From "${requestedFrom}" is not a configured sender identity.`
|
|
79291
|
+
};
|
|
79292
|
+
}
|
|
79102
79293
|
let attachments;
|
|
79103
79294
|
try {
|
|
79104
79295
|
attachments = buildAttachments(opts.attachments);
|
|
@@ -79114,7 +79305,7 @@ async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.de
|
|
|
79114
79305
|
const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
|
|
79115
79306
|
try {
|
|
79116
79307
|
const info = await transporter.sendMail({
|
|
79117
|
-
from:
|
|
79308
|
+
from: requestedFrom || cfg.from,
|
|
79118
79309
|
to: opts.to,
|
|
79119
79310
|
cc: opts.cc,
|
|
79120
79311
|
bcc: opts.bcc,
|
|
@@ -79324,6 +79515,27 @@ function decodeImapId(id) {
|
|
|
79324
79515
|
return null;
|
|
79325
79516
|
}
|
|
79326
79517
|
}
|
|
79518
|
+
function sameImapAccount(left, right, deps) {
|
|
79519
|
+
if (left === right) return true;
|
|
79520
|
+
if (deps.config) {
|
|
79521
|
+
const aliases = /* @__PURE__ */ new Set([deps.config.accountLabel, deps.config.user]);
|
|
79522
|
+
if (aliases.has(left) && aliases.has(right)) return true;
|
|
79523
|
+
}
|
|
79524
|
+
const specs = listImapAccountSpecs();
|
|
79525
|
+
const matches = (selector, spec) => spec.accountLabel === selector || spec.user === selector;
|
|
79526
|
+
const leftSpec = specs.find((spec) => matches(left, spec));
|
|
79527
|
+
const rightSpec = specs.find((spec) => matches(right, spec));
|
|
79528
|
+
return leftSpec !== void 0 && leftSpec === rightSpec;
|
|
79529
|
+
}
|
|
79530
|
+
function depsForAccount(account, deps) {
|
|
79531
|
+
if (deps.account && !sameImapAccount(account, deps.account, deps)) {
|
|
79532
|
+
throw new Error(`IMAP message id belongs to account "${account}", not "${deps.account}".`);
|
|
79533
|
+
}
|
|
79534
|
+
return { ...deps, account };
|
|
79535
|
+
}
|
|
79536
|
+
function depsForMessageRef(ref, deps) {
|
|
79537
|
+
return depsForAccount(ref.account, deps);
|
|
79538
|
+
}
|
|
79327
79539
|
function str(v) {
|
|
79328
79540
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
79329
79541
|
}
|
|
@@ -79831,25 +80043,21 @@ async function withMailbox(path, deps, fn) {
|
|
|
79831
80043
|
async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
79832
80044
|
const ref = decodeImapId(id);
|
|
79833
80045
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
79834
|
-
return withMailbox(
|
|
79835
|
-
|
|
79836
|
-
|
|
79837
|
-
|
|
79838
|
-
|
|
79839
|
-
|
|
79840
|
-
|
|
79841
|
-
|
|
79842
|
-
|
|
79843
|
-
|
|
79844
|
-
|
|
79845
|
-
|
|
79846
|
-
const src = msg.source ? msg.source.toString() : "";
|
|
79847
|
-
const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ?? extractTextBody(src) ?? extractHtmlBody(src) ?? "(no readable body)";
|
|
79848
|
-
return { success: true, info: `Subject: ${subject}
|
|
80046
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80047
|
+
const msg = await client.fetchOne(
|
|
80048
|
+
String(ref.uid),
|
|
80049
|
+
{ envelope: true, source: true },
|
|
80050
|
+
{ uid: true }
|
|
80051
|
+
);
|
|
80052
|
+
if (!msg)
|
|
80053
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
80054
|
+
const subject = msg.envelope?.subject || "(no subject)";
|
|
80055
|
+
const src = msg.source ? msg.source.toString() : "";
|
|
80056
|
+
const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ?? extractTextBody(src) ?? extractHtmlBody(src) ?? "(no readable body)";
|
|
80057
|
+
return { success: true, info: `Subject: ${subject}
|
|
79849
80058
|
|
|
79850
80059
|
${body}` };
|
|
79851
|
-
|
|
79852
|
-
);
|
|
80060
|
+
});
|
|
79853
80061
|
}
|
|
79854
80062
|
function normalizeMessageId(mid) {
|
|
79855
80063
|
return mid.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
@@ -79858,15 +80066,11 @@ async function imapFetchMessageId(id, deps = {}) {
|
|
|
79858
80066
|
const ref = decodeImapId(id);
|
|
79859
80067
|
if (!ref) return null;
|
|
79860
80068
|
try {
|
|
79861
|
-
return await withMailbox(
|
|
79862
|
-
ref.
|
|
79863
|
-
|
|
79864
|
-
|
|
79865
|
-
|
|
79866
|
-
const mid = msg && msg.envelope?.messageId;
|
|
79867
|
-
return mid ? normalizeMessageId(mid) : null;
|
|
79868
|
-
}
|
|
79869
|
-
);
|
|
80069
|
+
return await withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80070
|
+
const msg = await client.fetchOne(String(ref.uid), { envelope: true }, { uid: true });
|
|
80071
|
+
const mid = msg && msg.envelope?.messageId;
|
|
80072
|
+
return mid ? normalizeMessageId(mid) : null;
|
|
80073
|
+
});
|
|
79870
80074
|
} catch {
|
|
79871
80075
|
return null;
|
|
79872
80076
|
}
|
|
@@ -79874,23 +80078,19 @@ async function imapFetchMessageId(id, deps = {}) {
|
|
|
79874
80078
|
function flagOp(id, flag, add, deps) {
|
|
79875
80079
|
const ref = decodeImapId(id);
|
|
79876
80080
|
if (!ref) return Promise.resolve({ success: false, error: `Not an IMAP message id: "${id}".` });
|
|
79877
|
-
return withMailbox(
|
|
79878
|
-
|
|
79879
|
-
|
|
79880
|
-
|
|
79881
|
-
|
|
79882
|
-
|
|
79883
|
-
|
|
79884
|
-
|
|
79885
|
-
|
|
79886
|
-
|
|
79887
|
-
|
|
79888
|
-
success: false,
|
|
79889
|
-
error: `IMAP flag update failed for UID ${ref.uid}: ${errText(e)}`
|
|
79890
|
-
};
|
|
79891
|
-
}
|
|
80081
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80082
|
+
try {
|
|
80083
|
+
const ok = add ? await client.messageFlagsAdd([ref.uid], [flag], { uid: true }) : await client.messageFlagsRemove([ref.uid], [flag], { uid: true });
|
|
80084
|
+
if (!ok)
|
|
80085
|
+
return { success: false, error: `IMAP flag update returned false for UID ${ref.uid}.` };
|
|
80086
|
+
return { success: true };
|
|
80087
|
+
} catch (e) {
|
|
80088
|
+
return {
|
|
80089
|
+
success: false,
|
|
80090
|
+
error: `IMAP flag update failed for UID ${ref.uid}: ${errText(e)}`
|
|
80091
|
+
};
|
|
79892
80092
|
}
|
|
79893
|
-
);
|
|
80093
|
+
});
|
|
79894
80094
|
}
|
|
79895
80095
|
var imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
79896
80096
|
var imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|
|
@@ -79899,7 +80099,7 @@ var imapUnflagMessage = (id, deps = {}) => flagOp(id, "\\Flagged", false, deps);
|
|
|
79899
80099
|
async function imapMoveMessageById(id, destMailbox, deps = {}) {
|
|
79900
80100
|
const ref = decodeImapId(id);
|
|
79901
80101
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
79902
|
-
return withClient(
|
|
80102
|
+
return withClient(depsForMessageRef(ref, deps), async (client) => {
|
|
79903
80103
|
const destPath = await findMailboxPath(client, destMailbox) ?? resolveMailboxPath(destMailbox, "list");
|
|
79904
80104
|
const lock = await client.getMailboxLock(ref.path);
|
|
79905
80105
|
try {
|
|
@@ -79940,21 +80140,17 @@ async function trashUids(client, uids, srcPath) {
|
|
|
79940
80140
|
async function imapDeleteMessageById(id, deps = {}) {
|
|
79941
80141
|
const ref = decodeImapId(id);
|
|
79942
80142
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
79943
|
-
return withMailbox(
|
|
79944
|
-
|
|
79945
|
-
|
|
79946
|
-
|
|
79947
|
-
|
|
79948
|
-
|
|
79949
|
-
|
|
79950
|
-
|
|
79951
|
-
|
|
79952
|
-
};
|
|
79953
|
-
} catch (e) {
|
|
79954
|
-
return { success: false, error: `IMAP delete failed for UID ${ref.uid}: ${errText(e)}` };
|
|
79955
|
-
}
|
|
80143
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80144
|
+
try {
|
|
80145
|
+
const { dest, expunged } = await trashUids(client, [ref.uid], ref.path);
|
|
80146
|
+
return {
|
|
80147
|
+
success: true,
|
|
80148
|
+
info: expunged ? `Permanently deleted UID ${ref.uid} from Trash ("${ref.path}") via IMAP.` : `Moved UID ${ref.uid} to Trash ("${dest}") via IMAP.`
|
|
80149
|
+
};
|
|
80150
|
+
} catch (e) {
|
|
80151
|
+
return { success: false, error: `IMAP delete failed for UID ${ref.uid}: ${errText(e)}` };
|
|
79956
80152
|
}
|
|
79957
|
-
);
|
|
80153
|
+
});
|
|
79958
80154
|
}
|
|
79959
80155
|
function collectAttachments(node, out = []) {
|
|
79960
80156
|
if (!node) return out;
|
|
@@ -79980,54 +80176,46 @@ async function streamToBuffer(content) {
|
|
|
79980
80176
|
async function imapListAttachments(id, deps = {}) {
|
|
79981
80177
|
const ref = decodeImapId(id);
|
|
79982
80178
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
79983
|
-
return withMailbox(
|
|
79984
|
-
ref.
|
|
79985
|
-
|
|
79986
|
-
|
|
79987
|
-
|
|
79988
|
-
|
|
79989
|
-
|
|
79990
|
-
|
|
79991
|
-
|
|
79992
|
-
|
|
79993
|
-
|
|
79994
|
-
|
|
79995
|
-
|
|
79996
|
-
}));
|
|
79997
|
-
return { success: true, attachments };
|
|
79998
|
-
}
|
|
79999
|
-
);
|
|
80179
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80180
|
+
const msg = await client.fetchOne(String(ref.uid), { bodyStructure: true }, { uid: true });
|
|
80181
|
+
if (!msg || !msg.bodyStructure) {
|
|
80182
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
80183
|
+
}
|
|
80184
|
+
const attachments = collectAttachments(msg.bodyStructure).map((a) => ({
|
|
80185
|
+
id: `${id}#${a.part}`,
|
|
80186
|
+
name: a.filename,
|
|
80187
|
+
mimeType: a.mimeType,
|
|
80188
|
+
size: a.size
|
|
80189
|
+
}));
|
|
80190
|
+
return { success: true, attachments };
|
|
80191
|
+
});
|
|
80000
80192
|
}
|
|
80001
80193
|
async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
80002
80194
|
const ref = decodeImapId(id);
|
|
80003
80195
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
80004
|
-
return withMailbox(
|
|
80005
|
-
ref.
|
|
80006
|
-
|
|
80007
|
-
|
|
80008
|
-
|
|
80009
|
-
|
|
80010
|
-
|
|
80011
|
-
|
|
80012
|
-
const
|
|
80013
|
-
const match = atts.find((a) => a.filename === attachmentName);
|
|
80014
|
-
if (!match) {
|
|
80015
|
-
const names = atts.map((a) => a.filename).join(", ") || "none";
|
|
80016
|
-
return {
|
|
80017
|
-
success: false,
|
|
80018
|
-
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
|
|
80019
|
-
};
|
|
80020
|
-
}
|
|
80021
|
-
const dl = await client.download(String(ref.uid), match.part, { uid: true });
|
|
80022
|
-
const buf = await streamToBuffer(dl.content);
|
|
80196
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80197
|
+
const msg = await client.fetchOne(String(ref.uid), { bodyStructure: true }, { uid: true });
|
|
80198
|
+
if (!msg || !msg.bodyStructure) {
|
|
80199
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
80200
|
+
}
|
|
80201
|
+
const atts = collectAttachments(msg.bodyStructure);
|
|
80202
|
+
const match = atts.find((a) => a.filename === attachmentName);
|
|
80203
|
+
if (!match) {
|
|
80204
|
+
const names = atts.map((a) => a.filename).join(", ") || "none";
|
|
80023
80205
|
return {
|
|
80024
|
-
success:
|
|
80025
|
-
|
|
80026
|
-
bytes: buf.length,
|
|
80027
|
-
mimeType: match.mimeType
|
|
80206
|
+
success: false,
|
|
80207
|
+
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
|
|
80028
80208
|
};
|
|
80029
80209
|
}
|
|
80030
|
-
|
|
80210
|
+
const dl = await client.download(String(ref.uid), match.part, { uid: true });
|
|
80211
|
+
const buf = await streamToBuffer(dl.content);
|
|
80212
|
+
return {
|
|
80213
|
+
success: true,
|
|
80214
|
+
base64: buf.toString("base64"),
|
|
80215
|
+
bytes: buf.length,
|
|
80216
|
+
mimeType: match.mimeType
|
|
80217
|
+
};
|
|
80218
|
+
});
|
|
80031
80219
|
}
|
|
80032
80220
|
async function imapBatch(ids, deps, op) {
|
|
80033
80221
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -80048,7 +80236,7 @@ async function imapBatch(ids, deps, op) {
|
|
|
80048
80236
|
let success = 0;
|
|
80049
80237
|
for (const g of groups.values()) {
|
|
80050
80238
|
try {
|
|
80051
|
-
await useClient(
|
|
80239
|
+
await useClient(depsForAccount(g.account, deps), async (client) => {
|
|
80052
80240
|
const lock = await client.getMailboxLock(g.path);
|
|
80053
80241
|
try {
|
|
80054
80242
|
await op(client, g.uids, g.path);
|
|
@@ -80097,7 +80285,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
80097
80285
|
const ref = decodeImapId(id);
|
|
80098
80286
|
if (!ref) return null;
|
|
80099
80287
|
return useClient(
|
|
80100
|
-
|
|
80288
|
+
depsForMessageRef(ref, deps),
|
|
80101
80289
|
async (client) => {
|
|
80102
80290
|
const lock = await client.getMailboxLock(ref.path);
|
|
80103
80291
|
try {
|
|
@@ -80736,12 +80924,18 @@ var ATTACHMENTS_SCHEMA = external_exports.array(
|
|
|
80736
80924
|
external_exports.union([
|
|
80737
80925
|
external_exports.string().describe("Absolute path to an existing file"),
|
|
80738
80926
|
external_exports.object({
|
|
80739
|
-
filename: external_exports.string().min(1).describe("Filename to give the attachment"),
|
|
80740
|
-
contentBase64: external_exports.string().min(1).
|
|
80927
|
+
filename: external_exports.string().min(1).max(255).describe("Filename to give the attachment"),
|
|
80928
|
+
contentBase64: external_exports.string().min(1).max(
|
|
80929
|
+
MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS,
|
|
80930
|
+
"Inline attachment exceeds the 25 MiB decoded size limit"
|
|
80931
|
+
).refine(
|
|
80932
|
+
isInlineAttachmentBase64WithinLimit,
|
|
80933
|
+
"Inline attachment exceeds the 25 MiB decoded size limit"
|
|
80934
|
+
).describe("Base64-encoded file content (maximum 25 MiB decoded)")
|
|
80741
80935
|
})
|
|
80742
80936
|
])
|
|
80743
80937
|
).max(20, "Cannot attach more than 20 files").optional().describe(
|
|
80744
|
-
"Files to attach: absolute paths (e.g. '/Users/me/report.pdf') and/or inline {filename, contentBase64} objects
|
|
80938
|
+
"Files to attach: absolute paths (e.g. '/Users/me/report.pdf') and/or inline {filename, contentBase64} objects up to 25 MiB decoded each."
|
|
80745
80939
|
);
|
|
80746
80940
|
var MESSAGE_ROW_SCHEMA = external_exports.object({}).passthrough();
|
|
80747
80941
|
var LIST_OUTPUT_SCHEMA = {
|
|
@@ -80964,20 +81158,32 @@ ${messageList}${coverageBlock}`,
|
|
|
80964
81158
|
server.registerTool(
|
|
80965
81159
|
"get-message",
|
|
80966
81160
|
{
|
|
80967
|
-
description:
|
|
81161
|
+
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.
|
|
81162
|
+
Returns: the message subject, body (plain text by default, HTML when preferHtml is true), and its stable RFC Message-ID (rfcMessageId) for dedup/threading.
|
|
81163
|
+
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.
|
|
81164
|
+
Do not use when: you don't yet have an id (use search-messages or list-messages first), or you want the whole conversation (use get-thread).`,
|
|
80968
81165
|
inputSchema: {
|
|
80969
81166
|
id: MESSAGE_ID_SCHEMA,
|
|
80970
|
-
preferHtml: external_exports.boolean().optional().describe("Return the HTML body (extracted from the message source) instead of plain text")
|
|
81167
|
+
preferHtml: external_exports.boolean().optional().describe("Return the HTML body (extracted from the message source) instead of plain text"),
|
|
81168
|
+
mailbox: external_exports.string().optional().describe(
|
|
81169
|
+
'Mailbox that holds the message (e.g. "Sent Items"). Numeric ids are unique per mailbox; supplying this (with account) opens that mailbox directly instead of scanning every mailbox, which is required to read large folders like Sent Items without timing out.'
|
|
81170
|
+
),
|
|
81171
|
+
account: external_exports.string().optional().describe(
|
|
81172
|
+
"Account that holds the message. Pair with `mailbox` for a direct, scan-free fetch."
|
|
81173
|
+
)
|
|
80971
81174
|
},
|
|
80972
81175
|
outputSchema: {
|
|
80973
81176
|
id: external_exports.string().optional(),
|
|
80974
81177
|
subject: external_exports.string().optional(),
|
|
80975
81178
|
body: external_exports.string().optional(),
|
|
80976
|
-
isHtml: external_exports.boolean().optional()
|
|
81179
|
+
isHtml: external_exports.boolean().optional(),
|
|
81180
|
+
rfcMessageId: external_exports.string().optional().describe(
|
|
81181
|
+
"Stable RFC 5322 Message-ID (angle brackets stripped); empty when the message has none"
|
|
81182
|
+
)
|
|
80977
81183
|
}
|
|
80978
81184
|
},
|
|
80979
81185
|
withErrorHandling(
|
|
80980
|
-
({ id, preferHtml }) => routeMessage(id, {
|
|
81186
|
+
({ id, preferHtml, mailbox, account }) => routeMessage(id, {
|
|
80981
81187
|
// IMAP id (imap:…) → fetch via IMAP (#43 Phase 3); else AppleScript.
|
|
80982
81188
|
imap: () => imapGetMessage(id, preferHtml === true),
|
|
80983
81189
|
// IMAP path: parse subject/body out of the returned source so the
|
|
@@ -80989,11 +81195,15 @@ server.registerTool(
|
|
|
80989
81195
|
id,
|
|
80990
81196
|
subject: subjectFromGetMessage(r.info),
|
|
80991
81197
|
body: sep2 >= 0 ? r.info.slice(sep2 + 2) : r.info,
|
|
80992
|
-
isHtml: preferHtml === true
|
|
81198
|
+
isHtml: preferHtml === true,
|
|
81199
|
+
rfcMessageId: extractRfcMessageIdFromSource(r.info)
|
|
80993
81200
|
};
|
|
80994
81201
|
},
|
|
80995
81202
|
apple: () => {
|
|
80996
|
-
const content = mailManager.getMessageContent(id, preferHtml === true
|
|
81203
|
+
const content = mailManager.getMessageContent(id, preferHtml === true, {
|
|
81204
|
+
account,
|
|
81205
|
+
mailbox
|
|
81206
|
+
});
|
|
80997
81207
|
if (!content) return errorResponse(`Message with ID "${id}" not found`);
|
|
80998
81208
|
const isHtml = preferHtml === true && !!content.htmlContent;
|
|
80999
81209
|
const body = isHtml ? content.htmlContent : content.plainText;
|
|
@@ -81003,7 +81213,8 @@ ${body}`, {
|
|
|
81003
81213
|
id,
|
|
81004
81214
|
subject: content.subject,
|
|
81005
81215
|
body,
|
|
81006
|
-
isHtml
|
|
81216
|
+
isHtml,
|
|
81217
|
+
rfcMessageId: content.rfcMessageId ?? ""
|
|
81007
81218
|
});
|
|
81008
81219
|
},
|
|
81009
81220
|
ok: "",
|
|
@@ -81868,20 +82079,21 @@ server.registerTool(
|
|
|
81868
82079
|
if (/[/\\\0]/.test(attachmentName) || attachmentName.includes("..")) {
|
|
81869
82080
|
return errorResponse(`Invalid attachment name: "${attachmentName}"`);
|
|
81870
82081
|
}
|
|
81871
|
-
|
|
81872
|
-
|
|
81873
|
-
|
|
82082
|
+
let target;
|
|
82083
|
+
try {
|
|
82084
|
+
target = resolveAttachmentSaveTarget(savePath, attachmentName);
|
|
82085
|
+
} catch (error2) {
|
|
82086
|
+
return errorResponse(error2 instanceof Error ? error2.message : String(error2));
|
|
81874
82087
|
}
|
|
81875
82088
|
const r = await imapFetchAttachment(id, attachmentName);
|
|
81876
82089
|
if (!r.success || !r.base64) {
|
|
81877
82090
|
return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
|
|
81878
82091
|
}
|
|
81879
|
-
|
|
81880
|
-
writeFileSync4(savedPath, Buffer.from(r.base64, "base64"));
|
|
82092
|
+
writeFileSync4(target.savedPath, Buffer.from(r.base64, "base64"));
|
|
81881
82093
|
return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`, {
|
|
81882
82094
|
ok: true,
|
|
81883
82095
|
attachmentName,
|
|
81884
|
-
savedPath
|
|
82096
|
+
savedPath: target.savedPath
|
|
81885
82097
|
});
|
|
81886
82098
|
}
|
|
81887
82099
|
const success = mailManager.saveAttachment(id, attachmentName, savePath);
|
package/docs/IMAP-SETUP.md
CHANGED
|
@@ -199,6 +199,7 @@ the macOS 15+ blockquote wrapping. SMTP is single-account (the default sender):
|
|
|
199
199
|
"APPLE_MAIL_MCP_SMTP_PORT": "587",
|
|
200
200
|
"APPLE_MAIL_MCP_SMTP_USER": "you@gmail.com",
|
|
201
201
|
"APPLE_MAIL_MCP_SMTP_FROM": "you@gmail.com",
|
|
202
|
+
"APPLE_MAIL_MCP_SMTP_ALLOWED_FROM": "alias@example.com",
|
|
202
203
|
"APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE": "imap.gmail.com",
|
|
203
204
|
"APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT": "you@gmail.com"
|
|
204
205
|
}
|
package/package.json
CHANGED