apple-mail-mcp 2.8.9 → 2.8.11
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 +241 -138
- 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
|
|
|
@@ -76200,22 +76208,53 @@ var TemplateStore = class {
|
|
|
76200
76208
|
import { writeFileSync as writeFileSync2, rmSync, mkdtempSync } from "fs";
|
|
76201
76209
|
import { join as join2 } from "path";
|
|
76202
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
|
|
76203
76236
|
function materializeAttachments(attachments) {
|
|
76204
76237
|
if (!attachments || attachments.length === 0) {
|
|
76205
76238
|
return { paths: [], cleanup: () => void 0 };
|
|
76206
76239
|
}
|
|
76207
76240
|
let dir = null;
|
|
76208
|
-
|
|
76209
|
-
|
|
76210
|
-
|
|
76211
|
-
|
|
76212
|
-
|
|
76213
|
-
|
|
76214
|
-
|
|
76215
|
-
|
|
76216
|
-
|
|
76217
|
-
|
|
76218
|
-
|
|
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
|
+
}
|
|
76219
76258
|
return {
|
|
76220
76259
|
paths,
|
|
76221
76260
|
cleanup: () => {
|
|
@@ -76398,6 +76437,25 @@ function isPathWithinAllowedRoots(resolvedPath) {
|
|
|
76398
76437
|
return resolvedPath === base || resolvedPath.startsWith(base + sep);
|
|
76399
76438
|
});
|
|
76400
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
|
+
}
|
|
76401
76459
|
var UNSUPPORTED_APPLESCRIPT_OP = /AppleEvent handler failed|-10000/i;
|
|
76402
76460
|
function describeMailboxOpError(op, raw) {
|
|
76403
76461
|
const trimmed = (raw || "").trim();
|
|
@@ -77397,7 +77455,38 @@ var AppleMailManager = class {
|
|
|
77397
77455
|
let listCommand;
|
|
77398
77456
|
if (mailbox) {
|
|
77399
77457
|
const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
|
|
77400
|
-
|
|
77458
|
+
const gmailInbox = isInboxScope(mailbox) ? gmailReceivingMailboxes(this.getCachedMailboxNames(targetAccount)) : null;
|
|
77459
|
+
if (gmailInbox) {
|
|
77460
|
+
const nameList = appleScriptLowerNameList(gmailInbox);
|
|
77461
|
+
listCommand = `
|
|
77462
|
+
set outputText to ""
|
|
77463
|
+
set _timedOut to false
|
|
77464
|
+
set _notSearched to ""
|
|
77465
|
+
set _wantNames to ${nameList}
|
|
77466
|
+
set msgCount to 0
|
|
77467
|
+
set skipped to 0
|
|
77468
|
+
set seenIds to {}
|
|
77469
|
+
repeat with mb in mailboxes
|
|
77470
|
+
if msgCount >= ${limit} then exit repeat
|
|
77471
|
+
set mbName to ""
|
|
77472
|
+
try
|
|
77473
|
+
set mbName to name of mb
|
|
77474
|
+
end try
|
|
77475
|
+
ignoring case
|
|
77476
|
+
if _wantNames contains mbName then
|
|
77477
|
+
try
|
|
77478
|
+
${buildMessageRowLoop({ collection: `messages of mb ${fromFilter}`, limit, offset, dedup: true, withAttachments: true, trailing: ` & "${FIELD_SEP}" & mbName` })}
|
|
77479
|
+
on error _errMsg number _errNum
|
|
77480
|
+
set _timedOut to true
|
|
77481
|
+
set _notSearched to _notSearched & mbName & "${DIAG_ITEM_SEP}"
|
|
77482
|
+
end try
|
|
77483
|
+
end if
|
|
77484
|
+
end ignoring
|
|
77485
|
+
end repeat
|
|
77486
|
+
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=${DIAG_FIELD_SEP}notSearched=" & _notSearched
|
|
77487
|
+
`;
|
|
77488
|
+
} else {
|
|
77489
|
+
listCommand = `
|
|
77401
77490
|
set outputText to ""
|
|
77402
77491
|
set _timedOut to false
|
|
77403
77492
|
set _notSearched to ""
|
|
@@ -77412,6 +77501,7 @@ var AppleMailManager = class {
|
|
|
77412
77501
|
end try
|
|
77413
77502
|
return outputText & "${DIAG_MARKER}timedOut=" & (_timedOut as string) & "${DIAG_FIELD_SEP}skipped=${DIAG_FIELD_SEP}notSearched=" & _notSearched
|
|
77414
77503
|
`;
|
|
77504
|
+
}
|
|
77415
77505
|
} else {
|
|
77416
77506
|
const scanGuard = scanThreshold > 0 ? `mbCount > ${scanThreshold}` : "false";
|
|
77417
77507
|
listCommand = `
|
|
@@ -77917,7 +78007,7 @@ var AppleMailManager = class {
|
|
|
77917
78007
|
findNumericIdByMessageId(messageId, accountName) {
|
|
77918
78008
|
const mid = messageId.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
77919
78009
|
if (!mid) return null;
|
|
77920
|
-
const q = (s) => s
|
|
78010
|
+
const q = (s) => escapeForAppleScript(s);
|
|
77921
78011
|
const midLit = `"${q(mid)}"`;
|
|
77922
78012
|
const bracketedLit = `"${q(`<${mid}>`)}"`;
|
|
77923
78013
|
const matchClause = (mbVar) => `(messages of ${mbVar} whose message id is ${midLit} or message id is ${bracketedLit})`;
|
|
@@ -78304,13 +78394,15 @@ var AppleMailManager = class {
|
|
|
78304
78394
|
console.error(`Invalid attachment name: "${attachmentName}"`);
|
|
78305
78395
|
return false;
|
|
78306
78396
|
}
|
|
78307
|
-
|
|
78308
|
-
|
|
78309
|
-
|
|
78397
|
+
let target;
|
|
78398
|
+
try {
|
|
78399
|
+
target = resolveAttachmentSaveTarget(savePath, attachmentName);
|
|
78400
|
+
} catch (error2) {
|
|
78401
|
+
console.error(error2 instanceof Error ? error2.message : String(error2));
|
|
78310
78402
|
return false;
|
|
78311
78403
|
}
|
|
78312
78404
|
const safeName = escapeForAppleScript(attachmentName);
|
|
78313
|
-
const safePath = escapeForAppleScript(
|
|
78405
|
+
const safePath = escapeForAppleScript(target.saveDirectory);
|
|
78314
78406
|
const numericId = Number(id);
|
|
78315
78407
|
const script = buildAppLevelScript(`
|
|
78316
78408
|
try
|
|
@@ -78352,12 +78444,7 @@ var AppleMailManager = class {
|
|
|
78352
78444
|
return false;
|
|
78353
78445
|
}
|
|
78354
78446
|
try {
|
|
78355
|
-
|
|
78356
|
-
if (!isPathWithinAllowedRoots(outPath)) {
|
|
78357
|
-
console.error(`Output path "${outPath}" is outside allowed directories`);
|
|
78358
|
-
return false;
|
|
78359
|
-
}
|
|
78360
|
-
writeFileSync3(outPath, attachment.data);
|
|
78447
|
+
writeFileSync3(target.savedPath, attachment.data);
|
|
78361
78448
|
return true;
|
|
78362
78449
|
} catch (err) {
|
|
78363
78450
|
console.error(`Failed to write attachment to disk: ${err}`);
|
|
@@ -78374,7 +78461,7 @@ var AppleMailManager = class {
|
|
|
78374
78461
|
try {
|
|
78375
78462
|
dir = mkdtempSync2("/private/tmp/amcp-fetch-");
|
|
78376
78463
|
const dest = join4(dir, attachmentName.replace(/[/\\]/g, "_"));
|
|
78377
|
-
const ok = this.saveAttachment(id, attachmentName,
|
|
78464
|
+
const ok = this.saveAttachment(id, attachmentName, dir);
|
|
78378
78465
|
if (!ok) {
|
|
78379
78466
|
return {
|
|
78380
78467
|
success: false,
|
|
@@ -79125,7 +79212,7 @@ ${actionStmts.join("\n")}
|
|
|
79125
79212
|
|
|
79126
79213
|
// src/index.ts
|
|
79127
79214
|
import { writeFileSync as writeFileSync4 } from "fs";
|
|
79128
|
-
import {
|
|
79215
|
+
import { join as joinPath } from "path";
|
|
79129
79216
|
|
|
79130
79217
|
// src/services/smtpMailer.ts
|
|
79131
79218
|
var import_nodemailer = __toESM(require_nodemailer(), 1);
|
|
@@ -79144,6 +79231,7 @@ var SMTP_ENV = {
|
|
|
79144
79231
|
secure: "APPLE_MAIL_MCP_SMTP_SECURE",
|
|
79145
79232
|
user: "APPLE_MAIL_MCP_SMTP_USER",
|
|
79146
79233
|
from: "APPLE_MAIL_MCP_SMTP_FROM",
|
|
79234
|
+
allowedFrom: "APPLE_MAIL_MCP_SMTP_ALLOWED_FROM",
|
|
79147
79235
|
password: "APPLE_MAIL_MCP_SMTP_PASSWORD",
|
|
79148
79236
|
keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE",
|
|
79149
79237
|
keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT"
|
|
@@ -79189,6 +79277,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
79189
79277
|
throw new Error(`Invalid ${SMTP_ENV.port}: "${env[SMTP_ENV.port]}" is not a valid port.`);
|
|
79190
79278
|
}
|
|
79191
79279
|
const from = env[SMTP_ENV.from]?.trim() || user;
|
|
79280
|
+
const allowedFrom = (env[SMTP_ENV.allowedFrom] ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
79192
79281
|
let pass = env[SMTP_ENV.password];
|
|
79193
79282
|
if (!pass) {
|
|
79194
79283
|
const service = env[SMTP_ENV.keychainService]?.trim() || host;
|
|
@@ -79200,7 +79289,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
79200
79289
|
`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
|
|
79201
79290
|
);
|
|
79202
79291
|
}
|
|
79203
|
-
return { host, port, secure, user, pass, from };
|
|
79292
|
+
return { host, port, secure, user, pass, from, allowedFrom };
|
|
79204
79293
|
}
|
|
79205
79294
|
function buildAttachments(attachments) {
|
|
79206
79295
|
if (!attachments || attachments.length === 0) return void 0;
|
|
@@ -79213,7 +79302,7 @@ function buildAttachments(attachments) {
|
|
|
79213
79302
|
if (!a.filename || !a.contentBase64) {
|
|
79214
79303
|
throw new Error("Inline attachment requires both filename and contentBase64.");
|
|
79215
79304
|
}
|
|
79216
|
-
return { filename: a.filename, content:
|
|
79305
|
+
return { filename: a.filename, content: decodeInlineAttachment(a.contentBase64) };
|
|
79217
79306
|
});
|
|
79218
79307
|
}
|
|
79219
79308
|
async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.default.createTransport) {
|
|
@@ -79223,6 +79312,16 @@ async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.de
|
|
|
79223
79312
|
} catch (error2) {
|
|
79224
79313
|
return { success: false, error: error2 instanceof Error ? error2.message : String(error2) };
|
|
79225
79314
|
}
|
|
79315
|
+
const requestedFrom = opts.from?.trim();
|
|
79316
|
+
const allowedFrom = new Set(
|
|
79317
|
+
[cfg.user, cfg.from, ...cfg.allowedFrom ?? []].map((value) => value.trim().toLowerCase())
|
|
79318
|
+
);
|
|
79319
|
+
if (requestedFrom && !allowedFrom.has(requestedFrom.toLowerCase())) {
|
|
79320
|
+
return {
|
|
79321
|
+
success: false,
|
|
79322
|
+
error: `SMTP From "${requestedFrom}" is not a configured sender identity.`
|
|
79323
|
+
};
|
|
79324
|
+
}
|
|
79226
79325
|
let attachments;
|
|
79227
79326
|
try {
|
|
79228
79327
|
attachments = buildAttachments(opts.attachments);
|
|
@@ -79238,7 +79337,7 @@ async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.de
|
|
|
79238
79337
|
const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
|
|
79239
79338
|
try {
|
|
79240
79339
|
const info = await transporter.sendMail({
|
|
79241
|
-
from:
|
|
79340
|
+
from: requestedFrom || cfg.from,
|
|
79242
79341
|
to: opts.to,
|
|
79243
79342
|
cc: opts.cc,
|
|
79244
79343
|
bcc: opts.bcc,
|
|
@@ -79448,6 +79547,27 @@ function decodeImapId(id) {
|
|
|
79448
79547
|
return null;
|
|
79449
79548
|
}
|
|
79450
79549
|
}
|
|
79550
|
+
function sameImapAccount(left, right, deps) {
|
|
79551
|
+
if (left === right) return true;
|
|
79552
|
+
if (deps.config) {
|
|
79553
|
+
const aliases = /* @__PURE__ */ new Set([deps.config.accountLabel, deps.config.user]);
|
|
79554
|
+
if (aliases.has(left) && aliases.has(right)) return true;
|
|
79555
|
+
}
|
|
79556
|
+
const specs = listImapAccountSpecs();
|
|
79557
|
+
const matches = (selector, spec) => spec.accountLabel === selector || spec.user === selector;
|
|
79558
|
+
const leftSpec = specs.find((spec) => matches(left, spec));
|
|
79559
|
+
const rightSpec = specs.find((spec) => matches(right, spec));
|
|
79560
|
+
return leftSpec !== void 0 && leftSpec === rightSpec;
|
|
79561
|
+
}
|
|
79562
|
+
function depsForAccount(account, deps) {
|
|
79563
|
+
if (deps.account && !sameImapAccount(account, deps.account, deps)) {
|
|
79564
|
+
throw new Error(`IMAP message id belongs to account "${account}", not "${deps.account}".`);
|
|
79565
|
+
}
|
|
79566
|
+
return { ...deps, account };
|
|
79567
|
+
}
|
|
79568
|
+
function depsForMessageRef(ref, deps) {
|
|
79569
|
+
return depsForAccount(ref.account, deps);
|
|
79570
|
+
}
|
|
79451
79571
|
function str(v) {
|
|
79452
79572
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
79453
79573
|
}
|
|
@@ -79955,25 +80075,21 @@ async function withMailbox(path, deps, fn) {
|
|
|
79955
80075
|
async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
79956
80076
|
const ref = decodeImapId(id);
|
|
79957
80077
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
79958
|
-
return withMailbox(
|
|
79959
|
-
|
|
79960
|
-
|
|
79961
|
-
|
|
79962
|
-
|
|
79963
|
-
|
|
79964
|
-
|
|
79965
|
-
|
|
79966
|
-
|
|
79967
|
-
|
|
79968
|
-
|
|
79969
|
-
|
|
79970
|
-
const src = msg.source ? msg.source.toString() : "";
|
|
79971
|
-
const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ?? extractTextBody(src) ?? extractHtmlBody(src) ?? "(no readable body)";
|
|
79972
|
-
return { success: true, info: `Subject: ${subject}
|
|
80078
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80079
|
+
const msg = await client.fetchOne(
|
|
80080
|
+
String(ref.uid),
|
|
80081
|
+
{ envelope: true, source: true },
|
|
80082
|
+
{ uid: true }
|
|
80083
|
+
);
|
|
80084
|
+
if (!msg)
|
|
80085
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
80086
|
+
const subject = msg.envelope?.subject || "(no subject)";
|
|
80087
|
+
const src = msg.source ? msg.source.toString() : "";
|
|
80088
|
+
const body = (preferHtml ? extractHtmlBody(src) : extractTextBody(src)) ?? extractTextBody(src) ?? extractHtmlBody(src) ?? "(no readable body)";
|
|
80089
|
+
return { success: true, info: `Subject: ${subject}
|
|
79973
80090
|
|
|
79974
80091
|
${body}` };
|
|
79975
|
-
|
|
79976
|
-
);
|
|
80092
|
+
});
|
|
79977
80093
|
}
|
|
79978
80094
|
function normalizeMessageId(mid) {
|
|
79979
80095
|
return mid.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
@@ -79982,15 +80098,11 @@ async function imapFetchMessageId(id, deps = {}) {
|
|
|
79982
80098
|
const ref = decodeImapId(id);
|
|
79983
80099
|
if (!ref) return null;
|
|
79984
80100
|
try {
|
|
79985
|
-
return await withMailbox(
|
|
79986
|
-
ref.
|
|
79987
|
-
|
|
79988
|
-
|
|
79989
|
-
|
|
79990
|
-
const mid = msg && msg.envelope?.messageId;
|
|
79991
|
-
return mid ? normalizeMessageId(mid) : null;
|
|
79992
|
-
}
|
|
79993
|
-
);
|
|
80101
|
+
return await withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80102
|
+
const msg = await client.fetchOne(String(ref.uid), { envelope: true }, { uid: true });
|
|
80103
|
+
const mid = msg && msg.envelope?.messageId;
|
|
80104
|
+
return mid ? normalizeMessageId(mid) : null;
|
|
80105
|
+
});
|
|
79994
80106
|
} catch {
|
|
79995
80107
|
return null;
|
|
79996
80108
|
}
|
|
@@ -79998,23 +80110,19 @@ async function imapFetchMessageId(id, deps = {}) {
|
|
|
79998
80110
|
function flagOp(id, flag, add, deps) {
|
|
79999
80111
|
const ref = decodeImapId(id);
|
|
80000
80112
|
if (!ref) return Promise.resolve({ success: false, error: `Not an IMAP message id: "${id}".` });
|
|
80001
|
-
return withMailbox(
|
|
80002
|
-
|
|
80003
|
-
|
|
80004
|
-
|
|
80005
|
-
|
|
80006
|
-
|
|
80007
|
-
|
|
80008
|
-
|
|
80009
|
-
|
|
80010
|
-
|
|
80011
|
-
|
|
80012
|
-
success: false,
|
|
80013
|
-
error: `IMAP flag update failed for UID ${ref.uid}: ${errText(e)}`
|
|
80014
|
-
};
|
|
80015
|
-
}
|
|
80113
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80114
|
+
try {
|
|
80115
|
+
const ok = add ? await client.messageFlagsAdd([ref.uid], [flag], { uid: true }) : await client.messageFlagsRemove([ref.uid], [flag], { uid: true });
|
|
80116
|
+
if (!ok)
|
|
80117
|
+
return { success: false, error: `IMAP flag update returned false for UID ${ref.uid}.` };
|
|
80118
|
+
return { success: true };
|
|
80119
|
+
} catch (e) {
|
|
80120
|
+
return {
|
|
80121
|
+
success: false,
|
|
80122
|
+
error: `IMAP flag update failed for UID ${ref.uid}: ${errText(e)}`
|
|
80123
|
+
};
|
|
80016
80124
|
}
|
|
80017
|
-
);
|
|
80125
|
+
});
|
|
80018
80126
|
}
|
|
80019
80127
|
var imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
80020
80128
|
var imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|
|
@@ -80023,7 +80131,7 @@ var imapUnflagMessage = (id, deps = {}) => flagOp(id, "\\Flagged", false, deps);
|
|
|
80023
80131
|
async function imapMoveMessageById(id, destMailbox, deps = {}) {
|
|
80024
80132
|
const ref = decodeImapId(id);
|
|
80025
80133
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
80026
|
-
return withClient(
|
|
80134
|
+
return withClient(depsForMessageRef(ref, deps), async (client) => {
|
|
80027
80135
|
const destPath = await findMailboxPath(client, destMailbox) ?? resolveMailboxPath(destMailbox, "list");
|
|
80028
80136
|
const lock = await client.getMailboxLock(ref.path);
|
|
80029
80137
|
try {
|
|
@@ -80064,21 +80172,17 @@ async function trashUids(client, uids, srcPath) {
|
|
|
80064
80172
|
async function imapDeleteMessageById(id, deps = {}) {
|
|
80065
80173
|
const ref = decodeImapId(id);
|
|
80066
80174
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
80067
|
-
return withMailbox(
|
|
80068
|
-
|
|
80069
|
-
|
|
80070
|
-
|
|
80071
|
-
|
|
80072
|
-
|
|
80073
|
-
|
|
80074
|
-
|
|
80075
|
-
|
|
80076
|
-
};
|
|
80077
|
-
} catch (e) {
|
|
80078
|
-
return { success: false, error: `IMAP delete failed for UID ${ref.uid}: ${errText(e)}` };
|
|
80079
|
-
}
|
|
80175
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80176
|
+
try {
|
|
80177
|
+
const { dest, expunged } = await trashUids(client, [ref.uid], ref.path);
|
|
80178
|
+
return {
|
|
80179
|
+
success: true,
|
|
80180
|
+
info: expunged ? `Permanently deleted UID ${ref.uid} from Trash ("${ref.path}") via IMAP.` : `Moved UID ${ref.uid} to Trash ("${dest}") via IMAP.`
|
|
80181
|
+
};
|
|
80182
|
+
} catch (e) {
|
|
80183
|
+
return { success: false, error: `IMAP delete failed for UID ${ref.uid}: ${errText(e)}` };
|
|
80080
80184
|
}
|
|
80081
|
-
);
|
|
80185
|
+
});
|
|
80082
80186
|
}
|
|
80083
80187
|
function collectAttachments(node, out = []) {
|
|
80084
80188
|
if (!node) return out;
|
|
@@ -80104,54 +80208,46 @@ async function streamToBuffer(content) {
|
|
|
80104
80208
|
async function imapListAttachments(id, deps = {}) {
|
|
80105
80209
|
const ref = decodeImapId(id);
|
|
80106
80210
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
80107
|
-
return withMailbox(
|
|
80108
|
-
ref.
|
|
80109
|
-
|
|
80110
|
-
|
|
80111
|
-
|
|
80112
|
-
|
|
80113
|
-
|
|
80114
|
-
|
|
80115
|
-
|
|
80116
|
-
|
|
80117
|
-
|
|
80118
|
-
|
|
80119
|
-
|
|
80120
|
-
}));
|
|
80121
|
-
return { success: true, attachments };
|
|
80122
|
-
}
|
|
80123
|
-
);
|
|
80211
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80212
|
+
const msg = await client.fetchOne(String(ref.uid), { bodyStructure: true }, { uid: true });
|
|
80213
|
+
if (!msg || !msg.bodyStructure) {
|
|
80214
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
80215
|
+
}
|
|
80216
|
+
const attachments = collectAttachments(msg.bodyStructure).map((a) => ({
|
|
80217
|
+
id: `${id}#${a.part}`,
|
|
80218
|
+
name: a.filename,
|
|
80219
|
+
mimeType: a.mimeType,
|
|
80220
|
+
size: a.size
|
|
80221
|
+
}));
|
|
80222
|
+
return { success: true, attachments };
|
|
80223
|
+
});
|
|
80124
80224
|
}
|
|
80125
80225
|
async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
80126
80226
|
const ref = decodeImapId(id);
|
|
80127
80227
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
80128
|
-
return withMailbox(
|
|
80129
|
-
ref.
|
|
80130
|
-
|
|
80131
|
-
|
|
80132
|
-
|
|
80133
|
-
|
|
80134
|
-
|
|
80135
|
-
|
|
80136
|
-
const
|
|
80137
|
-
const match = atts.find((a) => a.filename === attachmentName);
|
|
80138
|
-
if (!match) {
|
|
80139
|
-
const names = atts.map((a) => a.filename).join(", ") || "none";
|
|
80140
|
-
return {
|
|
80141
|
-
success: false,
|
|
80142
|
-
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
|
|
80143
|
-
};
|
|
80144
|
-
}
|
|
80145
|
-
const dl = await client.download(String(ref.uid), match.part, { uid: true });
|
|
80146
|
-
const buf = await streamToBuffer(dl.content);
|
|
80228
|
+
return withMailbox(ref.path, depsForMessageRef(ref, deps), async (client) => {
|
|
80229
|
+
const msg = await client.fetchOne(String(ref.uid), { bodyStructure: true }, { uid: true });
|
|
80230
|
+
if (!msg || !msg.bodyStructure) {
|
|
80231
|
+
return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
|
|
80232
|
+
}
|
|
80233
|
+
const atts = collectAttachments(msg.bodyStructure);
|
|
80234
|
+
const match = atts.find((a) => a.filename === attachmentName);
|
|
80235
|
+
if (!match) {
|
|
80236
|
+
const names = atts.map((a) => a.filename).join(", ") || "none";
|
|
80147
80237
|
return {
|
|
80148
|
-
success:
|
|
80149
|
-
|
|
80150
|
-
bytes: buf.length,
|
|
80151
|
-
mimeType: match.mimeType
|
|
80238
|
+
success: false,
|
|
80239
|
+
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
|
|
80152
80240
|
};
|
|
80153
80241
|
}
|
|
80154
|
-
|
|
80242
|
+
const dl = await client.download(String(ref.uid), match.part, { uid: true });
|
|
80243
|
+
const buf = await streamToBuffer(dl.content);
|
|
80244
|
+
return {
|
|
80245
|
+
success: true,
|
|
80246
|
+
base64: buf.toString("base64"),
|
|
80247
|
+
bytes: buf.length,
|
|
80248
|
+
mimeType: match.mimeType
|
|
80249
|
+
};
|
|
80250
|
+
});
|
|
80155
80251
|
}
|
|
80156
80252
|
async function imapBatch(ids, deps, op) {
|
|
80157
80253
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -80172,7 +80268,7 @@ async function imapBatch(ids, deps, op) {
|
|
|
80172
80268
|
let success = 0;
|
|
80173
80269
|
for (const g of groups.values()) {
|
|
80174
80270
|
try {
|
|
80175
|
-
await useClient(
|
|
80271
|
+
await useClient(depsForAccount(g.account, deps), async (client) => {
|
|
80176
80272
|
const lock = await client.getMailboxLock(g.path);
|
|
80177
80273
|
try {
|
|
80178
80274
|
await op(client, g.uids, g.path);
|
|
@@ -80221,7 +80317,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
80221
80317
|
const ref = decodeImapId(id);
|
|
80222
80318
|
if (!ref) return null;
|
|
80223
80319
|
return useClient(
|
|
80224
|
-
|
|
80320
|
+
depsForMessageRef(ref, deps),
|
|
80225
80321
|
async (client) => {
|
|
80226
80322
|
const lock = await client.getMailboxLock(ref.path);
|
|
80227
80323
|
try {
|
|
@@ -80860,12 +80956,18 @@ var ATTACHMENTS_SCHEMA = external_exports.array(
|
|
|
80860
80956
|
external_exports.union([
|
|
80861
80957
|
external_exports.string().describe("Absolute path to an existing file"),
|
|
80862
80958
|
external_exports.object({
|
|
80863
|
-
filename: external_exports.string().min(1).describe("Filename to give the attachment"),
|
|
80864
|
-
contentBase64: external_exports.string().min(1).
|
|
80959
|
+
filename: external_exports.string().min(1).max(255).describe("Filename to give the attachment"),
|
|
80960
|
+
contentBase64: external_exports.string().min(1).max(
|
|
80961
|
+
MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS,
|
|
80962
|
+
"Inline attachment exceeds the 25 MiB decoded size limit"
|
|
80963
|
+
).refine(
|
|
80964
|
+
isInlineAttachmentBase64WithinLimit,
|
|
80965
|
+
"Inline attachment exceeds the 25 MiB decoded size limit"
|
|
80966
|
+
).describe("Base64-encoded file content (maximum 25 MiB decoded)")
|
|
80865
80967
|
})
|
|
80866
80968
|
])
|
|
80867
80969
|
).max(20, "Cannot attach more than 20 files").optional().describe(
|
|
80868
|
-
"Files to attach: absolute paths (e.g. '/Users/me/report.pdf') and/or inline {filename, contentBase64} objects
|
|
80970
|
+
"Files to attach: absolute paths (e.g. '/Users/me/report.pdf') and/or inline {filename, contentBase64} objects up to 25 MiB decoded each."
|
|
80869
80971
|
);
|
|
80870
80972
|
var MESSAGE_ROW_SCHEMA = external_exports.object({}).passthrough();
|
|
80871
80973
|
var LIST_OUTPUT_SCHEMA = {
|
|
@@ -82009,20 +82111,21 @@ server.registerTool(
|
|
|
82009
82111
|
if (/[/\\\0]/.test(attachmentName) || attachmentName.includes("..")) {
|
|
82010
82112
|
return errorResponse(`Invalid attachment name: "${attachmentName}"`);
|
|
82011
82113
|
}
|
|
82012
|
-
|
|
82013
|
-
|
|
82014
|
-
|
|
82114
|
+
let target;
|
|
82115
|
+
try {
|
|
82116
|
+
target = resolveAttachmentSaveTarget(savePath, attachmentName);
|
|
82117
|
+
} catch (error2) {
|
|
82118
|
+
return errorResponse(error2 instanceof Error ? error2.message : String(error2));
|
|
82015
82119
|
}
|
|
82016
82120
|
const r = await imapFetchAttachment(id, attachmentName);
|
|
82017
82121
|
if (!r.success || !r.base64) {
|
|
82018
82122
|
return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
|
|
82019
82123
|
}
|
|
82020
|
-
|
|
82021
|
-
writeFileSync4(savedPath, Buffer.from(r.base64, "base64"));
|
|
82124
|
+
writeFileSync4(target.savedPath, Buffer.from(r.base64, "base64"));
|
|
82022
82125
|
return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`, {
|
|
82023
82126
|
ok: true,
|
|
82024
82127
|
attachmentName,
|
|
82025
|
-
savedPath
|
|
82128
|
+
savedPath: target.savedPath
|
|
82026
82129
|
});
|
|
82027
82130
|
}
|
|
82028
82131
|
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