apple-mail-mcp 2.10.28 → 2.10.29

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 CHANGED
@@ -311,7 +311,7 @@ Send a new email immediately.
311
311
  | `cc` | string[] | No | CC recipients |
312
312
  | `bcc` | string[] | No | BCC recipients |
313
313
  | `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` |
314
- | `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 |
314
+ | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots (e.g., `"/Users/me/Documents/report.pdf"`) and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
315
315
  | `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) |
316
316
 
317
317
  **Example:**
@@ -665,7 +665,7 @@ Save an email to Drafts without sending.
665
665
  | `cc` | string[] | No | CC recipients |
666
666
  | `bcc` | string[] | No | BCC recipients |
667
667
  | `account` | string | No | Account for draft |
668
- | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
668
+ | `attachments` | (string \| {filename, contentBase64})[] | No | Up to 20 attachments: absolute file paths inside the configured read roots and/or inline `{filename, contentBase64}` objects up to 25 MiB decoded each |
669
669
 
670
670
  **Returns:** Confirmation that draft was created.
671
671
 
@@ -1604,6 +1604,16 @@ The entrypoint is written as:
1604
1604
  - **Permission required** - macOS will prompt for automation permission on first use.
1605
1605
  - **No credential storage** - The server doesn't store any passwords or authentication tokens.
1606
1606
  - **Email safety** - Use `create-draft` to review emails before sending.
1607
+ - **Attachment read boundary** - Outbound file attachments may come from
1608
+ ordinary files under the home directory, `/Volumes`, or temporary directories
1609
+ by default. Hidden files and known credential/configuration locations
1610
+ (including `.ssh`, `.aws`, `.config/gh`, Keychains, and
1611
+ application `config.json` files) are denied. Set
1612
+ `APPLE_MAIL_MCP_ATTACHMENT_READ_ROOTS` to a colon-separated list of explicit
1613
+ absolute additional roots when a deliberate other location is required.
1614
+ Paths are canonicalized before use, so symlink escapes are rejected. `/tmp`
1615
+ is world-writable and is a convenience root, not a user-content trust
1616
+ boundary. Inline base64 attachments are unaffected.
1607
1617
 
1608
1618
  ---
1609
1619
 
@@ -1613,7 +1623,7 @@ The entrypoint is written as:
1613
1623
  |------------|--------|
1614
1624
  | macOS only | Apple Mail and AppleScript are macOS-specific |
1615
1625
  | MCP `send-email` is plain-text | The `send-email` tool sends plain text (reading HTML content is supported). To send HTML, use the bundled `apple-mail-send` CLI with `--html-body-file` (sends `multipart/alternative` via SMTP) |
1616
- | Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
1626
+ | Attachment read path restrictions | Outbound file attachments must use full absolute paths inside the default home-directory, `/Volumes`, or temporary roots, except hidden files and protected credential/configuration locations. Set `APPLE_MAIL_MCP_ATTACHMENT_READ_ROOTS` to add an explicit absolute root for another deliberate location; symlink escapes are rejected. |
1617
1627
  | Smart mailboxes need Mail quit | Smart mailboxes are supported (see [Smart Mailbox Operations](#smart-mailbox-operations-intelligente-postfächer)), but `create-`/`delete-smart-mailbox` edit `SyncedSmartMailboxes.plist` directly — a running Mail may not show a new one until relaunched, and can overwrite plist edits it didn't make. Quit Mail first. Reading them needs Full Disk Access for the Node runtime |
1618
1628
  | Very large mailboxes not searchable *via AppleScript* | Apple Mail's AppleScript bridge times out on mailboxes with tens of thousands of messages, so unscoped `search-messages` skips mailboxes above `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default 5000) and reports them as a partial result. Scope with `mailbox` + a date window — or configure the [IMAP backend](#imap-backend--opt-in), which searches these server-side in well under a second. ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)) |
1619
1629
  | Can't delete/rename server-side mailboxes or mutate drafts *via AppleScript* | Mail.app's AppleScript bridge can only `delete`/`rename` **local "On My Mac"** mailboxes and cannot delete/move drafts — it throws `AppleEvent handler failed` for IMAP/Gmail/Workspace/iCloud/Exchange mailboxes (the GUI can do it). Without IMAP configured, `delete-mailbox`/`rename-mailbox`/`delete-message`/`move-message` return a clear "do it in Mail.app directly" error instead of a generic failure. With the [IMAP backend](#imap-backend--opt-in) configured for the account, these operations run via IMAP and succeed. ([#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) |
package/build/cli.js CHANGED
@@ -48,7 +48,7 @@ var require_punycode = __commonJS({
48
48
  var damp = 700;
49
49
  var initialBias = 72;
50
50
  var initialN = 128;
51
- var delimiter = "-";
51
+ var delimiter2 = "-";
52
52
  var regexPunycode = /^xn--/;
53
53
  var regexNonASCII = /[^\0-\x7F]/;
54
54
  var regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g;
@@ -139,7 +139,7 @@ var require_punycode = __commonJS({
139
139
  let i = 0;
140
140
  let n = initialN;
141
141
  let bias = initialBias;
142
- let basic = input.lastIndexOf(delimiter);
142
+ let basic = input.lastIndexOf(delimiter2);
143
143
  if (basic < 0) {
144
144
  basic = 0;
145
145
  }
@@ -199,7 +199,7 @@ var require_punycode = __commonJS({
199
199
  const basicLength = output.length;
200
200
  let handledCPCount = basicLength;
201
201
  if (basicLength) {
202
- output.push(delimiter);
202
+ output.push(delimiter2);
203
203
  }
204
204
  while (handledCPCount < inputLength) {
205
205
  let m = maxInt;
@@ -954,7 +954,7 @@ var require_shared = __commonJS({
954
954
  }
955
955
  return Object.keys(ifaces).map((key) => ifaces[key]).reduce((acc, val) => acc.concat(val), []).filter((i) => !i.internal || allowInternal).some((i) => i.family === "IPv" + family || i.family === family);
956
956
  };
957
- var resolve = (family, hostname, options, callback) => {
957
+ var resolve2 = (family, hostname, options, callback) => {
958
958
  options = options || {};
959
959
  if (!isFamilySupported(family, options.allowInternalNetworkInterfaces)) {
960
960
  return callback(null, []);
@@ -1041,13 +1041,13 @@ var require_shared = __commonJS({
1041
1041
  let ipv6Addresses = [];
1042
1042
  let ipv4Error = null;
1043
1043
  let ipv6Error = null;
1044
- resolve(4, options.host, options, (err, addresses) => {
1044
+ resolve2(4, options.host, options, (err, addresses) => {
1045
1045
  if (err) {
1046
1046
  ipv4Error = err;
1047
1047
  } else {
1048
1048
  ipv4Addresses = addresses || [];
1049
1049
  }
1050
- resolve(6, options.host, options, (err2, addresses2) => {
1050
+ resolve2(6, options.host, options, (err2, addresses2) => {
1051
1051
  if (err2) {
1052
1052
  ipv6Error = err2;
1053
1053
  } else {
@@ -1236,13 +1236,13 @@ var require_shared = __commonJS({
1236
1236
  });
1237
1237
  return response;
1238
1238
  };
1239
- module.exports.callbackPromise = (resolve2, reject) => function() {
1239
+ module.exports.callbackPromise = (resolve3, reject) => function() {
1240
1240
  const args = Array.from(arguments);
1241
1241
  const err = args.shift();
1242
1242
  if (err) {
1243
1243
  reject(err);
1244
1244
  } else {
1245
- resolve2(...args);
1245
+ resolve3(...args);
1246
1246
  }
1247
1247
  };
1248
1248
  module.exports.parseDataURI = (uri) => {
@@ -1309,8 +1309,8 @@ var require_shared = __commonJS({
1309
1309
  options = options || {};
1310
1310
  let promise;
1311
1311
  if (!callback) {
1312
- promise = new Promise((resolve2, reject) => {
1313
- callback = module.exports.callbackPromise(resolve2, reject);
1312
+ promise = new Promise((resolve3, reject) => {
1313
+ callback = module.exports.callbackPromise(resolve3, reject);
1314
1314
  });
1315
1315
  }
1316
1316
  resolveContentValue(data, key, options, callback);
@@ -4981,8 +4981,8 @@ var require_mime_node = __commonJS({
4981
4981
  build(callback) {
4982
4982
  let promise;
4983
4983
  if (!callback) {
4984
- promise = new Promise((resolve, reject) => {
4985
- callback = shared.callbackPromise(resolve, reject);
4984
+ promise = new Promise((resolve2, reject) => {
4985
+ callback = shared.callbackPromise(resolve2, reject);
4986
4986
  });
4987
4987
  }
4988
4988
  const stream = this.createReadStream();
@@ -7211,8 +7211,8 @@ var require_mailer = __commonJS({
7211
7211
  sendMail(data, callback = null) {
7212
7212
  let promise;
7213
7213
  if (!callback) {
7214
- promise = new Promise((resolve, reject) => {
7215
- callback = shared.callbackPromise(resolve, reject);
7214
+ promise = new Promise((resolve2, reject) => {
7215
+ callback = shared.callbackPromise(resolve2, reject);
7216
7216
  });
7217
7217
  }
7218
7218
  if (typeof this.getSocket === "function") {
@@ -7920,7 +7920,7 @@ var require_smtp_connection = __commonJS({
7920
7920
  const handler = this.customAuth.get(this._authMethod);
7921
7921
  let lastResponse;
7922
7922
  let returned = false;
7923
- const resolve = () => {
7923
+ const resolve2 = () => {
7924
7924
  if (returned) {
7925
7925
  return;
7926
7926
  }
@@ -7954,8 +7954,8 @@ var require_smtp_connection = __commonJS({
7954
7954
  sendCommand: (cmd, done) => {
7955
7955
  let promise;
7956
7956
  if (!done) {
7957
- promise = new Promise((resolve2, reject2) => {
7958
- done = shared.callbackPromise(resolve2, reject2);
7957
+ promise = new Promise((resolve3, reject2) => {
7958
+ done = shared.callbackPromise(resolve3, reject2);
7959
7959
  });
7960
7960
  }
7961
7961
  this._responseActions.push((str) => {
@@ -7980,11 +7980,11 @@ var require_smtp_connection = __commonJS({
7980
7980
  setImmediate(() => this._sendCommand(cmd));
7981
7981
  return promise;
7982
7982
  },
7983
- resolve,
7983
+ resolve: resolve2,
7984
7984
  reject
7985
7985
  });
7986
7986
  if (handlerResponse && typeof handlerResponse.catch === "function") {
7987
- handlerResponse.then(resolve).catch(reject);
7987
+ handlerResponse.then(resolve2).catch(reject);
7988
7988
  }
7989
7989
  return;
7990
7990
  }
@@ -10695,8 +10695,8 @@ var require_smtp_pool = __commonJS({
10695
10695
  verify(callback) {
10696
10696
  let promise;
10697
10697
  if (!callback) {
10698
- promise = new Promise((resolve, reject) => {
10699
- callback = shared.callbackPromise(resolve, reject);
10698
+ promise = new Promise((resolve2, reject) => {
10699
+ callback = shared.callbackPromise(resolve2, reject);
10700
10700
  });
10701
10701
  }
10702
10702
  const auth = new PoolResource(this).auth;
@@ -11043,8 +11043,8 @@ var require_smtp_transport = __commonJS({
11043
11043
  verify(callback) {
11044
11044
  let promise;
11045
11045
  if (!callback) {
11046
- promise = new Promise((resolve, reject) => {
11047
- callback = shared.callbackPromise(resolve, reject);
11046
+ promise = new Promise((resolve2, reject) => {
11047
+ callback = shared.callbackPromise(resolve2, reject);
11048
11048
  });
11049
11049
  }
11050
11050
  this.getSocket(this.options, (err, socketOptions) => {
@@ -11697,8 +11697,8 @@ var require_ses_transport = __commonJS({
11697
11697
  verify(callback) {
11698
11698
  let promise;
11699
11699
  if (!callback) {
11700
- promise = new Promise((resolve, reject) => {
11701
- callback = shared.callbackPromise(resolve, reject);
11700
+ promise = new Promise((resolve2, reject) => {
11701
+ callback = shared.callbackPromise(resolve2, reject);
11702
11702
  });
11703
11703
  }
11704
11704
  const cb = (err) => {
@@ -11799,8 +11799,8 @@ var require_nodemailer = __commonJS({
11799
11799
  apiUrl = false;
11800
11800
  }
11801
11801
  if (!callback) {
11802
- promise = new Promise((resolve, reject) => {
11803
- callback = shared.callbackPromise(resolve, reject);
11802
+ promise = new Promise((resolve2, reject) => {
11803
+ callback = shared.callbackPromise(resolve2, reject);
11804
11804
  });
11805
11805
  }
11806
11806
  if (ETHEREAL_CACHE && testAccount) {
@@ -11877,15 +11877,13 @@ var require_nodemailer = __commonJS({
11877
11877
  });
11878
11878
 
11879
11879
  // src/cli.ts
11880
- import { readFileSync, realpathSync } from "fs";
11880
+ import { readFileSync, realpathSync as realpathSync2 } from "fs";
11881
11881
  import { fileURLToPath } from "url";
11882
11882
  import { parseArgs } from "util";
11883
11883
 
11884
11884
  // src/services/smtpMailer.ts
11885
11885
  var import_nodemailer = __toESM(require_nodemailer(), 1);
11886
11886
  import { execFileSync } from "child_process";
11887
- import { isAbsolute } from "path";
11888
- import { existsSync } from "fs";
11889
11887
 
11890
11888
  // src/utils/attachmentLimits.ts
11891
11889
  var MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024;
@@ -11910,6 +11908,99 @@ function decodeInlineAttachment(contentBase64) {
11910
11908
  return content;
11911
11909
  }
11912
11910
 
11911
+ // src/utils/attachmentReadPolicy.ts
11912
+ import { realpathSync, statSync } from "fs";
11913
+ import { homedir, tmpdir } from "os";
11914
+ import { delimiter, isAbsolute, join, resolve, sep } from "path";
11915
+ var ATTACHMENT_READ_ROOTS_ENV = "APPLE_MAIL_MCP_ATTACHMENT_READ_ROOTS";
11916
+ var DEFAULT_ATTACHMENT_READ_ROOTS = [homedir(), "/Volumes", tmpdir(), "/tmp", "/private/tmp"];
11917
+ var SENSITIVE_HOME_ROOTS = [
11918
+ join(homedir(), ".ssh"),
11919
+ join(homedir(), ".aws"),
11920
+ join(homedir(), ".config", "gh"),
11921
+ join(homedir(), "Library", "Keychains")
11922
+ ];
11923
+ function canonicalize(path) {
11924
+ return realpathSync.native(path);
11925
+ }
11926
+ function sensitiveRoots() {
11927
+ return SENSITIVE_HOME_ROOTS.map((root) => {
11928
+ try {
11929
+ return canonicalize(root);
11930
+ } catch {
11931
+ return root;
11932
+ }
11933
+ });
11934
+ }
11935
+ function isWithinRoot(candidate, root) {
11936
+ return candidate === root || candidate.startsWith(root + sep);
11937
+ }
11938
+ function hasHiddenPathSegment(candidate) {
11939
+ return candidate.split(sep).some((segment) => segment.startsWith(".") && segment.length > 1);
11940
+ }
11941
+ function isProtectedPath(candidate) {
11942
+ if (hasHiddenPathSegment(candidate)) return true;
11943
+ if (sensitiveRoots().some((root) => isWithinRoot(candidate, root))) return true;
11944
+ let home;
11945
+ try {
11946
+ home = canonicalize(homedir());
11947
+ } catch {
11948
+ home = resolve(homedir());
11949
+ }
11950
+ if (!isWithinRoot(candidate, home)) return false;
11951
+ const relative = candidate.slice(home.length).split(sep).filter(Boolean);
11952
+ return relative.length >= 4 && relative[0].toLowerCase() === "library" && relative[1].toLowerCase() === "application support" && relative.at(-1)?.toLowerCase() === "config.json";
11953
+ }
11954
+ function configuredRoots(env) {
11955
+ const raw = env[ATTACHMENT_READ_ROOTS_ENV];
11956
+ const extraRoots = raw === void 0 ? [] : raw.split(delimiter).map((root) => root.trim()).filter(Boolean);
11957
+ const requested = [...DEFAULT_ATTACHMENT_READ_ROOTS, ...extraRoots];
11958
+ for (const root of requested) {
11959
+ if (!isAbsolute(root)) {
11960
+ throw new Error(`${ATTACHMENT_READ_ROOTS_ENV} entries must be absolute paths.`);
11961
+ }
11962
+ }
11963
+ const resolved = [];
11964
+ for (const root of requested) {
11965
+ try {
11966
+ const canonical = canonicalize(resolve(root));
11967
+ if (!resolved.includes(canonical)) resolved.push(canonical);
11968
+ } catch {
11969
+ }
11970
+ }
11971
+ return resolved;
11972
+ }
11973
+ function resolveAttachmentReadPath(filePath, env = process.env) {
11974
+ if (!isAbsolute(filePath)) {
11975
+ throw new Error(`Attachment path must be absolute: "${filePath}"`);
11976
+ }
11977
+ let canonical;
11978
+ try {
11979
+ canonical = canonicalize(filePath);
11980
+ } catch {
11981
+ throw new Error(`Attachment file not found: "${filePath}"`);
11982
+ }
11983
+ try {
11984
+ if (!statSync(canonical).isFile()) {
11985
+ throw new Error(`Attachment path is not a regular file: "${filePath}"`);
11986
+ }
11987
+ } catch (error) {
11988
+ if (error instanceof Error && error.message.includes("not a regular file")) throw error;
11989
+ throw new Error(`Attachment file not found: "${filePath}"`);
11990
+ }
11991
+ if (isProtectedPath(canonical)) {
11992
+ throw new Error(
11993
+ `Attachment path is in a protected location: "${filePath}". Hidden files and credential/configuration locations cannot be sent as attachments.`
11994
+ );
11995
+ }
11996
+ if (!configuredRoots(env).some((root) => isWithinRoot(canonical, root))) {
11997
+ throw new Error(
11998
+ `Attachment path is outside the allowed read roots: "${filePath}". Use an ordinary home-directory, /Volumes, or temporary path, or configure ${ATTACHMENT_READ_ROOTS_ENV} for an additional explicit root.`
11999
+ );
12000
+ }
12001
+ return canonical;
12002
+ }
12003
+
11913
12004
  // src/utils/docsUrls.ts
11914
12005
  var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
11915
12006
  var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
@@ -11986,9 +12077,7 @@ function buildAttachments(attachments) {
11986
12077
  if (!attachments || attachments.length === 0) return void 0;
11987
12078
  return attachments.map((a) => {
11988
12079
  if (typeof a === "string") {
11989
- if (!isAbsolute(a)) throw new Error(`Attachment path must be absolute: "${a}"`);
11990
- if (!existsSync(a)) throw new Error(`Attachment file not found: "${a}"`);
11991
- return { path: a };
12080
+ return { path: resolveAttachmentReadPath(a) };
11992
12081
  }
11993
12082
  if (!a.filename || !a.contentBase64) {
11994
12083
  throw new Error("Inline attachment requires both filename and contentBase64.");
@@ -12171,7 +12260,7 @@ async function runCli(argv, deps = {}) {
12171
12260
  function isInvokedDirectly() {
12172
12261
  if (typeof process === "undefined" || !process.argv?.[1]) return false;
12173
12262
  try {
12174
- return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
12263
+ return realpathSync2(process.argv[1]) === fileURLToPath(import.meta.url);
12175
12264
  } catch {
12176
12265
  return false;
12177
12266
  }