apple-mail-mcp 2.8.9 → 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 +208 -137
- 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();
|
|
@@ -77917,7 +77975,7 @@ var AppleMailManager = class {
|
|
|
77917
77975
|
findNumericIdByMessageId(messageId, accountName) {
|
|
77918
77976
|
const mid = messageId.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
77919
77977
|
if (!mid) return null;
|
|
77920
|
-
const q = (s) => s
|
|
77978
|
+
const q = (s) => escapeForAppleScript(s);
|
|
77921
77979
|
const midLit = `"${q(mid)}"`;
|
|
77922
77980
|
const bracketedLit = `"${q(`<${mid}>`)}"`;
|
|
77923
77981
|
const matchClause = (mbVar) => `(messages of ${mbVar} whose message id is ${midLit} or message id is ${bracketedLit})`;
|
|
@@ -78304,13 +78362,15 @@ var AppleMailManager = class {
|
|
|
78304
78362
|
console.error(`Invalid attachment name: "${attachmentName}"`);
|
|
78305
78363
|
return false;
|
|
78306
78364
|
}
|
|
78307
|
-
|
|
78308
|
-
|
|
78309
|
-
|
|
78365
|
+
let target;
|
|
78366
|
+
try {
|
|
78367
|
+
target = resolveAttachmentSaveTarget(savePath, attachmentName);
|
|
78368
|
+
} catch (error2) {
|
|
78369
|
+
console.error(error2 instanceof Error ? error2.message : String(error2));
|
|
78310
78370
|
return false;
|
|
78311
78371
|
}
|
|
78312
78372
|
const safeName = escapeForAppleScript(attachmentName);
|
|
78313
|
-
const safePath = escapeForAppleScript(
|
|
78373
|
+
const safePath = escapeForAppleScript(target.saveDirectory);
|
|
78314
78374
|
const numericId = Number(id);
|
|
78315
78375
|
const script = buildAppLevelScript(`
|
|
78316
78376
|
try
|
|
@@ -78352,12 +78412,7 @@ var AppleMailManager = class {
|
|
|
78352
78412
|
return false;
|
|
78353
78413
|
}
|
|
78354
78414
|
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);
|
|
78415
|
+
writeFileSync3(target.savedPath, attachment.data);
|
|
78361
78416
|
return true;
|
|
78362
78417
|
} catch (err) {
|
|
78363
78418
|
console.error(`Failed to write attachment to disk: ${err}`);
|
|
@@ -78374,7 +78429,7 @@ var AppleMailManager = class {
|
|
|
78374
78429
|
try {
|
|
78375
78430
|
dir = mkdtempSync2("/private/tmp/amcp-fetch-");
|
|
78376
78431
|
const dest = join4(dir, attachmentName.replace(/[/\\]/g, "_"));
|
|
78377
|
-
const ok = this.saveAttachment(id, attachmentName,
|
|
78432
|
+
const ok = this.saveAttachment(id, attachmentName, dir);
|
|
78378
78433
|
if (!ok) {
|
|
78379
78434
|
return {
|
|
78380
78435
|
success: false,
|
|
@@ -79125,7 +79180,7 @@ ${actionStmts.join("\n")}
|
|
|
79125
79180
|
|
|
79126
79181
|
// src/index.ts
|
|
79127
79182
|
import { writeFileSync as writeFileSync4 } from "fs";
|
|
79128
|
-
import {
|
|
79183
|
+
import { join as joinPath } from "path";
|
|
79129
79184
|
|
|
79130
79185
|
// src/services/smtpMailer.ts
|
|
79131
79186
|
var import_nodemailer = __toESM(require_nodemailer(), 1);
|
|
@@ -79144,6 +79199,7 @@ var SMTP_ENV = {
|
|
|
79144
79199
|
secure: "APPLE_MAIL_MCP_SMTP_SECURE",
|
|
79145
79200
|
user: "APPLE_MAIL_MCP_SMTP_USER",
|
|
79146
79201
|
from: "APPLE_MAIL_MCP_SMTP_FROM",
|
|
79202
|
+
allowedFrom: "APPLE_MAIL_MCP_SMTP_ALLOWED_FROM",
|
|
79147
79203
|
password: "APPLE_MAIL_MCP_SMTP_PASSWORD",
|
|
79148
79204
|
keychainService: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE",
|
|
79149
79205
|
keychainAccount: "APPLE_MAIL_MCP_SMTP_KEYCHAIN_ACCOUNT"
|
|
@@ -79189,6 +79245,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
79189
79245
|
throw new Error(`Invalid ${SMTP_ENV.port}: "${env[SMTP_ENV.port]}" is not a valid port.`);
|
|
79190
79246
|
}
|
|
79191
79247
|
const from = env[SMTP_ENV.from]?.trim() || user;
|
|
79248
|
+
const allowedFrom = (env[SMTP_ENV.allowedFrom] ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
79192
79249
|
let pass = env[SMTP_ENV.password];
|
|
79193
79250
|
if (!pass) {
|
|
79194
79251
|
const service = env[SMTP_ENV.keychainService]?.trim() || host;
|
|
@@ -79200,7 +79257,7 @@ function resolveSmtpConfig(env = process.env) {
|
|
|
79200
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
|
|
79201
79258
|
);
|
|
79202
79259
|
}
|
|
79203
|
-
return { host, port, secure, user, pass, from };
|
|
79260
|
+
return { host, port, secure, user, pass, from, allowedFrom };
|
|
79204
79261
|
}
|
|
79205
79262
|
function buildAttachments(attachments) {
|
|
79206
79263
|
if (!attachments || attachments.length === 0) return void 0;
|
|
@@ -79213,7 +79270,7 @@ function buildAttachments(attachments) {
|
|
|
79213
79270
|
if (!a.filename || !a.contentBase64) {
|
|
79214
79271
|
throw new Error("Inline attachment requires both filename and contentBase64.");
|
|
79215
79272
|
}
|
|
79216
|
-
return { filename: a.filename, content:
|
|
79273
|
+
return { filename: a.filename, content: decodeInlineAttachment(a.contentBase64) };
|
|
79217
79274
|
});
|
|
79218
79275
|
}
|
|
79219
79276
|
async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.default.createTransport) {
|
|
@@ -79223,6 +79280,16 @@ async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.de
|
|
|
79223
79280
|
} catch (error2) {
|
|
79224
79281
|
return { success: false, error: error2 instanceof Error ? error2.message : String(error2) };
|
|
79225
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
|
+
}
|
|
79226
79293
|
let attachments;
|
|
79227
79294
|
try {
|
|
79228
79295
|
attachments = buildAttachments(opts.attachments);
|
|
@@ -79238,7 +79305,7 @@ async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.de
|
|
|
79238
79305
|
const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
|
|
79239
79306
|
try {
|
|
79240
79307
|
const info = await transporter.sendMail({
|
|
79241
|
-
from:
|
|
79308
|
+
from: requestedFrom || cfg.from,
|
|
79242
79309
|
to: opts.to,
|
|
79243
79310
|
cc: opts.cc,
|
|
79244
79311
|
bcc: opts.bcc,
|
|
@@ -79448,6 +79515,27 @@ function decodeImapId(id) {
|
|
|
79448
79515
|
return null;
|
|
79449
79516
|
}
|
|
79450
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
|
+
}
|
|
79451
79539
|
function str(v) {
|
|
79452
79540
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
79453
79541
|
}
|
|
@@ -79955,25 +80043,21 @@ async function withMailbox(path, deps, fn) {
|
|
|
79955
80043
|
async function imapGetMessage(id, preferHtml, deps = {}) {
|
|
79956
80044
|
const ref = decodeImapId(id);
|
|
79957
80045
|
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}
|
|
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}
|
|
79973
80058
|
|
|
79974
80059
|
${body}` };
|
|
79975
|
-
|
|
79976
|
-
);
|
|
80060
|
+
});
|
|
79977
80061
|
}
|
|
79978
80062
|
function normalizeMessageId(mid) {
|
|
79979
80063
|
return mid.trim().replace(/^<+/, "").replace(/>+$/, "").trim();
|
|
@@ -79982,15 +80066,11 @@ async function imapFetchMessageId(id, deps = {}) {
|
|
|
79982
80066
|
const ref = decodeImapId(id);
|
|
79983
80067
|
if (!ref) return null;
|
|
79984
80068
|
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
|
-
);
|
|
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
|
+
});
|
|
79994
80074
|
} catch {
|
|
79995
80075
|
return null;
|
|
79996
80076
|
}
|
|
@@ -79998,23 +80078,19 @@ async function imapFetchMessageId(id, deps = {}) {
|
|
|
79998
80078
|
function flagOp(id, flag, add, deps) {
|
|
79999
80079
|
const ref = decodeImapId(id);
|
|
80000
80080
|
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
|
-
}
|
|
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
|
+
};
|
|
80016
80092
|
}
|
|
80017
|
-
);
|
|
80093
|
+
});
|
|
80018
80094
|
}
|
|
80019
80095
|
var imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
|
|
80020
80096
|
var imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
|
|
@@ -80023,7 +80099,7 @@ var imapUnflagMessage = (id, deps = {}) => flagOp(id, "\\Flagged", false, deps);
|
|
|
80023
80099
|
async function imapMoveMessageById(id, destMailbox, deps = {}) {
|
|
80024
80100
|
const ref = decodeImapId(id);
|
|
80025
80101
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
80026
|
-
return withClient(
|
|
80102
|
+
return withClient(depsForMessageRef(ref, deps), async (client) => {
|
|
80027
80103
|
const destPath = await findMailboxPath(client, destMailbox) ?? resolveMailboxPath(destMailbox, "list");
|
|
80028
80104
|
const lock = await client.getMailboxLock(ref.path);
|
|
80029
80105
|
try {
|
|
@@ -80064,21 +80140,17 @@ async function trashUids(client, uids, srcPath) {
|
|
|
80064
80140
|
async function imapDeleteMessageById(id, deps = {}) {
|
|
80065
80141
|
const ref = decodeImapId(id);
|
|
80066
80142
|
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
|
-
}
|
|
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)}` };
|
|
80080
80152
|
}
|
|
80081
|
-
);
|
|
80153
|
+
});
|
|
80082
80154
|
}
|
|
80083
80155
|
function collectAttachments(node, out = []) {
|
|
80084
80156
|
if (!node) return out;
|
|
@@ -80104,54 +80176,46 @@ async function streamToBuffer(content) {
|
|
|
80104
80176
|
async function imapListAttachments(id, deps = {}) {
|
|
80105
80177
|
const ref = decodeImapId(id);
|
|
80106
80178
|
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
|
-
);
|
|
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
|
+
});
|
|
80124
80192
|
}
|
|
80125
80193
|
async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
80126
80194
|
const ref = decodeImapId(id);
|
|
80127
80195
|
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);
|
|
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";
|
|
80147
80205
|
return {
|
|
80148
|
-
success:
|
|
80149
|
-
|
|
80150
|
-
bytes: buf.length,
|
|
80151
|
-
mimeType: match.mimeType
|
|
80206
|
+
success: false,
|
|
80207
|
+
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
|
|
80152
80208
|
};
|
|
80153
80209
|
}
|
|
80154
|
-
|
|
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
|
+
});
|
|
80155
80219
|
}
|
|
80156
80220
|
async function imapBatch(ids, deps, op) {
|
|
80157
80221
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -80172,7 +80236,7 @@ async function imapBatch(ids, deps, op) {
|
|
|
80172
80236
|
let success = 0;
|
|
80173
80237
|
for (const g of groups.values()) {
|
|
80174
80238
|
try {
|
|
80175
|
-
await useClient(
|
|
80239
|
+
await useClient(depsForAccount(g.account, deps), async (client) => {
|
|
80176
80240
|
const lock = await client.getMailboxLock(g.path);
|
|
80177
80241
|
try {
|
|
80178
80242
|
await op(client, g.uids, g.path);
|
|
@@ -80221,7 +80285,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
80221
80285
|
const ref = decodeImapId(id);
|
|
80222
80286
|
if (!ref) return null;
|
|
80223
80287
|
return useClient(
|
|
80224
|
-
|
|
80288
|
+
depsForMessageRef(ref, deps),
|
|
80225
80289
|
async (client) => {
|
|
80226
80290
|
const lock = await client.getMailboxLock(ref.path);
|
|
80227
80291
|
try {
|
|
@@ -80860,12 +80924,18 @@ var ATTACHMENTS_SCHEMA = external_exports.array(
|
|
|
80860
80924
|
external_exports.union([
|
|
80861
80925
|
external_exports.string().describe("Absolute path to an existing file"),
|
|
80862
80926
|
external_exports.object({
|
|
80863
|
-
filename: external_exports.string().min(1).describe("Filename to give the attachment"),
|
|
80864
|
-
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)")
|
|
80865
80935
|
})
|
|
80866
80936
|
])
|
|
80867
80937
|
).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
|
|
80938
|
+
"Files to attach: absolute paths (e.g. '/Users/me/report.pdf') and/or inline {filename, contentBase64} objects up to 25 MiB decoded each."
|
|
80869
80939
|
);
|
|
80870
80940
|
var MESSAGE_ROW_SCHEMA = external_exports.object({}).passthrough();
|
|
80871
80941
|
var LIST_OUTPUT_SCHEMA = {
|
|
@@ -82009,20 +82079,21 @@ server.registerTool(
|
|
|
82009
82079
|
if (/[/\\\0]/.test(attachmentName) || attachmentName.includes("..")) {
|
|
82010
82080
|
return errorResponse(`Invalid attachment name: "${attachmentName}"`);
|
|
82011
82081
|
}
|
|
82012
|
-
|
|
82013
|
-
|
|
82014
|
-
|
|
82082
|
+
let target;
|
|
82083
|
+
try {
|
|
82084
|
+
target = resolveAttachmentSaveTarget(savePath, attachmentName);
|
|
82085
|
+
} catch (error2) {
|
|
82086
|
+
return errorResponse(error2 instanceof Error ? error2.message : String(error2));
|
|
82015
82087
|
}
|
|
82016
82088
|
const r = await imapFetchAttachment(id, attachmentName);
|
|
82017
82089
|
if (!r.success || !r.base64) {
|
|
82018
82090
|
return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
|
|
82019
82091
|
}
|
|
82020
|
-
|
|
82021
|
-
writeFileSync4(savedPath, Buffer.from(r.base64, "base64"));
|
|
82092
|
+
writeFileSync4(target.savedPath, Buffer.from(r.base64, "base64"));
|
|
82022
82093
|
return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`, {
|
|
82023
82094
|
ok: true,
|
|
82024
82095
|
attachmentName,
|
|
82025
|
-
savedPath
|
|
82096
|
+
savedPath: target.savedPath
|
|
82026
82097
|
});
|
|
82027
82098
|
}
|
|
82028
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