apple-mail-mcp 2.10.26 → 2.10.28

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
@@ -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`,
@@ -1085,10 +1099,15 @@ Create a Mail rule with one or more conditions and actions.
1085
1099
  | `conditions` | object[] | Yes | One or more `{field, operator, value}` (see below) |
1086
1100
  | `actions` | object | Yes | At least one of `markRead`, `markFlagged`, `delete`, `moveTo` |
1087
1101
  | `matchAll` | boolean | No | `true` (default) = all conditions must match; `false` = any |
1088
- | `enabled` | boolean | No | Whether the rule is enabled on creation (default `true`) |
1102
+ | `enabled` | boolean | No | Whether the rule is enabled on creation (default `false`) |
1089
1103
 
1090
1104
  Each condition is `{ field, operator, value }` where `field` is one of `from`, `to`, `cc`, `subject`, `content` and `operator` is one of `contains`, `notContains`, `equals`, `beginsWith`, `endsWith`. Actions: `markRead` / `markFlagged` / `delete` (booleans), `moveTo` (mailbox name) with optional `moveToAccount`.
1091
1105
 
1106
+ New rules are created **disabled by default**, including rules that delete or move
1107
+ messages. Review the conditions and actions with `list-rules` and in Mail.app,
1108
+ then call `enable-rule` explicitly when the rule is approved. Set
1109
+ `enabled: true` only when immediate activation is deliberate.
1110
+
1092
1111
  **Example:**
1093
1112
  ```json
1094
1113
  {
package/build/cli.js CHANGED
@@ -11919,6 +11919,7 @@ var SMTP_ENV = {
11919
11919
  host: "APPLE_MAIL_MCP_SMTP_HOST",
11920
11920
  port: "APPLE_MAIL_MCP_SMTP_PORT",
11921
11921
  secure: "APPLE_MAIL_MCP_SMTP_SECURE",
11922
+ allowPlaintext: "APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT",
11922
11923
  user: "APPLE_MAIL_MCP_SMTP_USER",
11923
11924
  from: "APPLE_MAIL_MCP_SMTP_FROM",
11924
11925
  allowedFrom: "APPLE_MAIL_MCP_SMTP_ALLOWED_FROM",
@@ -11969,7 +11970,17 @@ function resolveSmtpConfig(env = process.env) {
11969
11970
  `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
11971
  );
11971
11972
  }
11972
- return { host, port, secure, user, pass, from, allowedFrom };
11973
+ const allowPlaintext = /^(1|true|yes|on)$/i.test(env[SMTP_ENV.allowPlaintext]?.trim() ?? "");
11974
+ return {
11975
+ host,
11976
+ port,
11977
+ secure,
11978
+ allowPlaintext,
11979
+ user,
11980
+ pass,
11981
+ from,
11982
+ allowedFrom
11983
+ };
11973
11984
  }
11974
11985
  function buildAttachments(attachments) {
11975
11986
  if (!attachments || attachments.length === 0) return void 0;
@@ -12008,10 +12019,19 @@ async function sendViaSmtp(opts, config, createTransport = import_nodemailer.def
12008
12019
  } catch (error) {
12009
12020
  return { success: false, error: error instanceof Error ? error.message : String(error) };
12010
12021
  }
12022
+ const requireTLS = !cfg.secure && !cfg.allowPlaintext;
12023
+ if (!cfg.secure && cfg.allowPlaintext) {
12024
+ console.warn(
12025
+ `SMTP plaintext explicitly enabled via ${SMTP_ENV.allowPlaintext}; credentials and message content may be exposed.`
12026
+ );
12027
+ }
12011
12028
  const transporter = createTransport({
12012
12029
  host: cfg.host,
12013
12030
  port: cfg.port,
12014
12031
  secure: cfg.secure,
12032
+ // Port 587/143-style configurations must not silently downgrade to
12033
+ // plaintext when the server advertises no usable TLS upgrade.
12034
+ requireTLS,
12015
12035
  auth: { user: cfg.user, pass: cfg.pass }
12016
12036
  });
12017
12037
  const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
@@ -12032,9 +12052,11 @@ async function sendViaSmtp(opts, config, createTransport = import_nodemailer.def
12032
12052
  });
12033
12053
  return { success: true, messageId: info.messageId };
12034
12054
  } catch (error) {
12055
+ const detail = error instanceof Error ? error.message : String(error);
12056
+ const tlsHint = requireTLS ? ` STARTTLS is required for non-implicit TLS; to explicitly allow plaintext (not recommended), set ${SMTP_ENV.allowPlaintext}=1.` : "";
12035
12057
  return {
12036
12058
  success: false,
12037
- error: `SMTP send failed: ${error instanceof Error ? error.message : String(error)}`
12059
+ error: `SMTP send failed: ${detail}.${tlsHint}`
12038
12060
  };
12039
12061
  } finally {
12040
12062
  transporter.close();
package/build/index.js CHANGED
@@ -82068,7 +82068,7 @@ end tell`;
82068
82068
  if (!actionStmts.length) {
82069
82069
  return { success: false, error: "A rule needs at least one action." };
82070
82070
  }
82071
- const enabled = opts.enabled !== false;
82071
+ const enabled = opts.enabled === true;
82072
82072
  const matchAll = opts.matchAll !== false;
82073
82073
  const script = buildAppLevelScript(`
82074
82074
  try
@@ -82446,6 +82446,7 @@ var SMTP_ENV = {
82446
82446
  host: "APPLE_MAIL_MCP_SMTP_HOST",
82447
82447
  port: "APPLE_MAIL_MCP_SMTP_PORT",
82448
82448
  secure: "APPLE_MAIL_MCP_SMTP_SECURE",
82449
+ allowPlaintext: "APPLE_MAIL_MCP_SMTP_ALLOW_PLAINTEXT",
82449
82450
  user: "APPLE_MAIL_MCP_SMTP_USER",
82450
82451
  from: "APPLE_MAIL_MCP_SMTP_FROM",
82451
82452
  allowedFrom: "APPLE_MAIL_MCP_SMTP_ALLOWED_FROM",
@@ -82506,7 +82507,17 @@ function resolveSmtpConfig(env = process.env) {
82506
82507
  `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
82507
82508
  );
82508
82509
  }
82509
- return { host, port, secure, user, pass, from, allowedFrom };
82510
+ const allowPlaintext = /^(1|true|yes|on)$/i.test(env[SMTP_ENV.allowPlaintext]?.trim() ?? "");
82511
+ return {
82512
+ host,
82513
+ port,
82514
+ secure,
82515
+ allowPlaintext,
82516
+ user,
82517
+ pass,
82518
+ from,
82519
+ allowedFrom
82520
+ };
82510
82521
  }
82511
82522
  function buildAttachments(attachments) {
82512
82523
  if (!attachments || attachments.length === 0) return void 0;
@@ -82545,10 +82556,19 @@ async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.de
82545
82556
  } catch (error2) {
82546
82557
  return { success: false, error: error2 instanceof Error ? error2.message : String(error2) };
82547
82558
  }
82559
+ const requireTLS = !cfg.secure && !cfg.allowPlaintext;
82560
+ if (!cfg.secure && cfg.allowPlaintext) {
82561
+ console.warn(
82562
+ `SMTP plaintext explicitly enabled via ${SMTP_ENV.allowPlaintext}; credentials and message content may be exposed.`
82563
+ );
82564
+ }
82548
82565
  const transporter = createTransport({
82549
82566
  host: cfg.host,
82550
82567
  port: cfg.port,
82551
82568
  secure: cfg.secure,
82569
+ // Port 587/143-style configurations must not silently downgrade to
82570
+ // plaintext when the server advertises no usable TLS upgrade.
82571
+ requireTLS,
82552
82572
  auth: { user: cfg.user, pass: cfg.pass }
82553
82573
  });
82554
82574
  const html = opts.htmlBody?.trim() ? opts.htmlBody : void 0;
@@ -82569,9 +82589,11 @@ async function sendViaSmtp(opts, config2, createTransport = import_nodemailer.de
82569
82589
  });
82570
82590
  return { success: true, messageId: info.messageId };
82571
82591
  } catch (error2) {
82592
+ const detail = error2 instanceof Error ? error2.message : String(error2);
82593
+ const tlsHint = requireTLS ? ` STARTTLS is required for non-implicit TLS; to explicitly allow plaintext (not recommended), set ${SMTP_ENV.allowPlaintext}=1.` : "";
82572
82594
  return {
82573
82595
  success: false,
82574
- error: `SMTP send failed: ${error2 instanceof Error ? error2.message : String(error2)}`
82596
+ error: `SMTP send failed: ${detail}.${tlsHint}`
82575
82597
  };
82576
82598
  } finally {
82577
82599
  transporter.close();
@@ -82744,6 +82766,7 @@ var IMAP_ENV = {
82744
82766
  password: "APPLE_MAIL_MCP_IMAP_PASSWORD",
82745
82767
  keychainService: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE",
82746
82768
  keychainAccount: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT",
82769
+ allowPlaintext: "APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT",
82747
82770
  // C2 multi-account: JSON array of additional accounts, e.g.
82748
82771
  // [{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]
82749
82772
  accounts: "APPLE_MAIL_MCP_IMAP_ACCOUNTS"
@@ -82784,6 +82807,9 @@ function depsForAccount(account, deps) {
82784
82807
  function depsForMessageRef(ref, deps) {
82785
82808
  return depsForAccount(ref.account, deps);
82786
82809
  }
82810
+ function isTruthySetting(value) {
82811
+ return /^(1|true|yes|on)$/i.test(value?.trim() ?? "");
82812
+ }
82787
82813
  function specMatchesSelector(spec, selector) {
82788
82814
  return spec.accountLabel === selector || spec.user === selector || (spec.aliases?.includes(selector) ?? false);
82789
82815
  }
@@ -82849,7 +82875,7 @@ function listImapAccountSpecs(env = process.env) {
82849
82875
  }
82850
82876
  return specs;
82851
82877
  }
82852
- function specToConfig(spec) {
82878
+ function specToConfig(spec, allowPlaintext = false) {
82853
82879
  if (!Number.isInteger(spec.port) || spec.port <= 0) {
82854
82880
  throw new Error(`Invalid IMAP port for account "${spec.accountLabel}": "${spec.port}".`);
82855
82881
  }
@@ -82866,6 +82892,7 @@ function specToConfig(spec) {
82866
82892
  host: spec.host,
82867
82893
  port: spec.port,
82868
82894
  secure: spec.port === 993,
82895
+ allowPlaintext,
82869
82896
  user: spec.user,
82870
82897
  pass,
82871
82898
  accountLabel: spec.accountLabel
@@ -82883,9 +82910,10 @@ function listImapAccountLabels(env = process.env) {
82883
82910
  }
82884
82911
  function resolveImapConfigs(env = process.env) {
82885
82912
  const out = [];
82913
+ const allowPlaintext = isTruthySetting(env[IMAP_ENV.allowPlaintext]);
82886
82914
  for (const spec of listImapAccountSpecs(env)) {
82887
82915
  try {
82888
- out.push(specToConfig(spec));
82916
+ out.push(specToConfig(spec, allowPlaintext));
82889
82917
  } catch (e) {
82890
82918
  console.error(`Skipping IMAP account "${spec.accountLabel}": ${String(e)}`);
82891
82919
  }
@@ -82910,19 +82938,45 @@ function resolveImapConfig(env = process.env, account) {
82910
82938
  } else {
82911
82939
  spec = specs[0];
82912
82940
  }
82913
- return specToConfig(spec);
82941
+ return specToConfig(spec, isTruthySetting(env[IMAP_ENV.allowPlaintext]));
82914
82942
  }
82915
- var defaultConnect = async (cfg) => {
82916
- const client = new import_imapflow.ImapFlow({
82943
+ function buildImapConnectionOptions(cfg) {
82944
+ return {
82917
82945
  host: cfg.host,
82918
82946
  port: cfg.port,
82919
82947
  secure: cfg.secure,
82948
+ // ImapFlow reads this as a tri-state, and the distinction matters:
82949
+ // true -> require STARTTLS; fail if the server does not offer it
82950
+ // false -> NEVER STARTTLS, even if the server advertises it
82951
+ // undefined -> opportunistic upgrade (ImapFlow's documented default)
82952
+ //
82953
+ // secure=true already has implicit TLS, so there is no upgrade to negotiate.
82954
+ // Without the escape hatch the upgrade is required. WITH it we must fall back
82955
+ // to `undefined`, not `false`: the escape hatch means "let me reach a server
82956
+ // that cannot do TLS", not "never encrypt". Sending `false` suppressed the
82957
+ // upgrade even against servers still offering it, so enabling the opt-out for
82958
+ // one broken account silently downgraded every other plaintext-port account
82959
+ // below what it already negotiated before this option existed.
82960
+ doSTARTTLS: cfg.secure || cfg.allowPlaintext ? void 0 : true,
82920
82961
  auth: { user: cfg.user, pass: cfg.pass },
82921
82962
  logger: false
82922
- });
82963
+ };
82964
+ }
82965
+ var defaultConnect = async (cfg) => {
82966
+ const client = new import_imapflow.ImapFlow(buildImapConnectionOptions(cfg));
82923
82967
  client.on("error", () => {
82924
82968
  });
82925
- await client.connect();
82969
+ try {
82970
+ await client.connect();
82971
+ } catch (error2) {
82972
+ if (!cfg.secure && !cfg.allowPlaintext) {
82973
+ const detail = error2 instanceof Error ? error2.message : String(error2);
82974
+ throw new Error(
82975
+ `IMAP connection failed: ${detail}. STARTTLS is required for non-implicit TLS; to explicitly allow plaintext (not recommended), set ${IMAP_ENV.allowPlaintext}=1.`
82976
+ );
82977
+ }
82978
+ throw error2;
82979
+ }
82926
82980
  return client;
82927
82981
  };
82928
82982
  function resolveMailboxPath(mailbox, mode) {
@@ -84214,16 +84268,20 @@ function subjectFromGetMessage(info) {
84214
84268
  // src/services/imapIdle.ts
84215
84269
  var import_imapflow2 = __toESM(require_imap_flow(), 1);
84216
84270
  var defaultIdleConnect = async (cfg) => {
84217
- const client = new import_imapflow2.ImapFlow({
84218
- host: cfg.host,
84219
- port: cfg.port,
84220
- secure: cfg.secure,
84221
- auth: { user: cfg.user, pass: cfg.pass },
84222
- logger: false
84223
- });
84271
+ const client = new import_imapflow2.ImapFlow(buildImapConnectionOptions(cfg));
84224
84272
  client.on("error", () => {
84225
84273
  });
84226
- await client.connect();
84274
+ try {
84275
+ await client.connect();
84276
+ } catch (error2) {
84277
+ if (!cfg.secure && !cfg.allowPlaintext) {
84278
+ const detail = error2 instanceof Error ? error2.message : String(error2);
84279
+ throw new Error(
84280
+ `IMAP IDLE connection failed: ${detail}. STARTTLS is required for non-implicit TLS; to explicitly allow plaintext (not recommended), set ${IMAP_ENV.allowPlaintext}=1.`
84281
+ );
84282
+ }
84283
+ throw error2;
84284
+ }
84227
84285
  return client;
84228
84286
  };
84229
84287
  var ImapIdleWatcher = class {
@@ -86249,7 +86307,7 @@ registerTool(
86249
86307
  "At least one action is required (markRead, markFlagged, delete, or moveTo)"
86250
86308
  ),
86251
86309
  matchAll: external_exports.boolean().default(true),
86252
- enabled: external_exports.boolean().default(true)
86310
+ enabled: external_exports.boolean().default(false).describe("Enable immediately; defaults to false so the rule must be reviewed first")
86253
86311
  },
86254
86312
  outputSchema: {
86255
86313
  name: external_exports.string().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.10.26",
3
+ "version": "2.10.28",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",