apple-mail-mcp 2.10.27 → 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:**
@@ -358,12 +358,19 @@ fallback.
358
358
  Configure SMTP via environment variables on the MCP server. The password is
359
359
  read from the macOS **Keychain** by default, so no secret goes in config:
360
360
 
361
+ Non-implicit-TLS SMTP connections fail closed if STARTTLS is unavailable.
362
+ `APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT=1` is a deliberate escape hatch for a
363
+ trusted isolated server or test fixture; it disables the upgrade requirement and
364
+ can expose credentials and message content. The server emits a warning when it
365
+ is used. Keep the default unset.
366
+
361
367
  | Variable | Required | Default | Description |
362
368
  |----------|----------|---------|-------------|
363
369
  | `APPLE_MAIL_MCP_SMTP_HOST` | Yes | — | SMTP server hostname (e.g. `smtp.fastmail.com`) |
364
370
  | `APPLE_MAIL_MCP_SMTP_USER` | Yes | — | SMTP username |
365
371
  | `APPLE_MAIL_MCP_SMTP_PORT` | No | `465` if secure, else `587` | SMTP port |
366
372
  | `APPLE_MAIL_MCP_SMTP_SECURE` | No | `false` | `true` for implicit TLS (port 465); otherwise STARTTLS |
373
+ | `APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
367
374
  | `APPLE_MAIL_MCP_SMTP_FROM` | No | = user | From address |
368
375
  | `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` | No | — | Comma-separated sender aliases permitted as per-message From overrides |
369
376
  | `APPLE_MAIL_MCP_SMTP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
@@ -480,6 +487,7 @@ for an explicitly-named IMAP account, never on an omitted account.
480
487
  | `APPLE_MAIL_MCP_IMAP_ACCOUNT` | No | = user | Mail account name to match for routing |
481
488
  | `APPLE_MAIL_MCP_IMAP_HOST` | No | `imap.gmail.com` | IMAP server hostname |
482
489
  | `APPLE_MAIL_MCP_IMAP_PORT` | No | `993` | IMAP port (993 = implicit TLS) |
490
+ | `APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
483
491
  | `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
484
492
  | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name |
485
493
  | `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
@@ -495,6 +503,12 @@ Each entry accepts `account`, `user`, `host`, `port`, `password`, `keychainServi
495
503
  `keychainAccount`. Calls route to the account matching their `account` argument (or the
496
504
  decoded `imap:` id), and each account keeps its own pooled connection.
497
505
 
506
+ Non-implicit-TLS IMAP connections require STARTTLS and fail closed when the server
507
+ does not offer a usable upgrade. `APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT=1` is a
508
+ deliberate escape hatch for a trusted isolated server or test fixture; it disables
509
+ the upgrade requirement and can expose credentials and message content. Keep the
510
+ default unset.
511
+
498
512
  As with SMTP, the password is read from the macOS **Keychain** by default (use
499
513
  an app-specific password for Gmail/Workspace/iCloud), so no secret goes in
500
514
  config. Gmail label semantics: common names (`All Mail`, `Sent`, `Trash`,
@@ -651,7 +665,7 @@ Save an email to Drafts without sending.
651
665
  | `cc` | string[] | No | CC recipients |
652
666
  | `bcc` | string[] | No | BCC recipients |
653
667
  | `account` | string | No | Account for draft |
654
- | `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 |
655
669
 
656
670
  **Returns:** Confirmation that draft was created.
657
671
 
@@ -1590,6 +1604,16 @@ The entrypoint is written as:
1590
1604
  - **Permission required** - macOS will prompt for automation permission on first use.
1591
1605
  - **No credential storage** - The server doesn't store any passwords or authentication tokens.
1592
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.
1593
1617
 
1594
1618
  ---
1595
1619
 
@@ -1599,7 +1623,7 @@ The entrypoint is written as:
1599
1623
  |------------|--------|
1600
1624
  | macOS only | Apple Mail and AppleScript are macOS-specific |
1601
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) |
1602
- | 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. |
1603
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 |
1604
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)) |
1605
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.`;
@@ -11919,6 +12010,7 @@ var SMTP_ENV = {
11919
12010
  host: "APPLE_MAIL_MCP_SMTP_HOST",
11920
12011
  port: "APPLE_MAIL_MCP_SMTP_PORT",
11921
12012
  secure: "APPLE_MAIL_MCP_SMTP_SECURE",
12013
+ allowPlaintext: "APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT",
11922
12014
  user: "APPLE_MAIL_MCP_SMTP_USER",
11923
12015
  from: "APPLE_MAIL_MCP_SMTP_FROM",
11924
12016
  allowedFrom: "APPLE_MAIL_MCP_SMTP_ALLOWED_FROM",
@@ -11969,15 +12061,23 @@ function resolveSmtpConfig(env = process.env) {
11969
12061
  `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
11970
12062
  );
11971
12063
  }
11972
- return { host, port, secure, user, pass, from, allowedFrom };
12064
+ const allowPlaintext = /^(1|true|yes|on)$/i.test(env[SMTP_ENV.allowPlaintext]?.trim() ?? "");
12065
+ return {
12066
+ host,
12067
+ port,
12068
+ secure,
12069
+ allowPlaintext,
12070
+ user,
12071
+ pass,
12072
+ from,
12073
+ allowedFrom
12074
+ };
11973
12075
  }
11974
12076
  function buildAttachments(attachments) {
11975
12077
  if (!attachments || attachments.length === 0) return void 0;
11976
12078
  return attachments.map((a) => {
11977
12079
  if (typeof a === "string") {
11978
- if (!isAbsolute(a)) throw new Error(`Attachment path must be absolute: "${a}"`);
11979
- if (!existsSync(a)) throw new Error(`Attachment file not found: "${a}"`);
11980
- return { path: a };
12080
+ return { path: resolveAttachmentReadPath(a) };
11981
12081
  }
11982
12082
  if (!a.filename || !a.contentBase64) {
11983
12083
  throw new Error("Inline attachment requires both filename and contentBase64.");
@@ -12008,10 +12108,19 @@ async function sendViaSmtp(opts, config, createTransport = import_nodemailer.def
12008
12108
  } catch (error) {
12009
12109
  return { success: false, error: error instanceof Error ? error.message : String(error) };
12010
12110
  }
12111
+ const requireTLS = !cfg.secure && !cfg.allowPlaintext;
12112
+ if (!cfg.secure && cfg.allowPlaintext) {
12113
+ console.warn(
12114
+ `SMTP plaintext explicitly enabled via ${SMTP_ENV.allowPlaintext}; credentials and message content may be exposed.`
12115
+ );
12116
+ }
12011
12117
  const transporter = createTransport({
12012
12118
  host: cfg.host,
12013
12119
  port: cfg.port,
12014
12120
  secure: cfg.secure,
12121
+ // Port 587/143-style configurations must not silently downgrade to
12122
+ // plaintext when the server advertises no usable TLS upgrade.
12123
+ requireTLS,
12015
12124
  auth: { user: cfg.user, pass: cfg.pass }
12016
12125
  });
12017
12126
  const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
@@ -12032,9 +12141,11 @@ async function sendViaSmtp(opts, config, createTransport = import_nodemailer.def
12032
12141
  });
12033
12142
  return { success: true, messageId: info.messageId };
12034
12143
  } catch (error) {
12144
+ const detail = error instanceof Error ? error.message : String(error);
12145
+ const tlsHint = requireTLS ? ` STARTTLS is required for non-implicit TLS; to explicitly allow plaintext (not recommended), set ${SMTP_ENV.allowPlaintext}=1.` : "";
12035
12146
  return {
12036
12147
  success: false,
12037
- error: `SMTP send failed: ${error instanceof Error ? error.message : String(error)}`
12148
+ error: `SMTP send failed: ${detail}.${tlsHint}`
12038
12149
  };
12039
12150
  } finally {
12040
12151
  transporter.close();
@@ -12149,7 +12260,7 @@ async function runCli(argv, deps = {}) {
12149
12260
  function isInvokedDirectly() {
12150
12261
  if (typeof process === "undefined" || !process.argv?.[1]) return false;
12151
12262
  try {
12152
- return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
12263
+ return realpathSync2(process.argv[1]) === fileURLToPath(import.meta.url);
12153
12264
  } catch {
12154
12265
  return false;
12155
12266
  }