apple-mail-mcp 2.20.12 → 2.20.14

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/build/index.js +200 -99
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -443,7 +443,7 @@ Send a new email immediately.
443
443
  | `body` | string | Yes | Email body (plain text) |
444
444
  | `cc` | string[] | No | CC recipients |
445
445
  | `bcc` | string[] | No | BCC recipients |
446
- | `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` |
446
+ | `account` | string | No | Mail.app account name or exact sender address (AppleScript path: resolved to one address of an enabled account and verified against Mail's `sender` before sending — no match, or Mail would use another sender, is an error and nothing is sent; the sender used is returned as `from`), 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` |
447
447
  | `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 |
448
448
  | `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) |
449
449
  | `replyTo` | string | No | SMTP only: `Reply-To` header, when replies should go somewhere other than the From address. Ignored on the AppleScript transport |
package/build/index.js CHANGED
@@ -65211,6 +65211,30 @@ function isInlineDisposition(headers) {
65211
65211
  if (!dispHeader) return false;
65212
65212
  return dispHeader.toLowerCase().startsWith("inline");
65213
65213
  }
65214
+ function extractContentId(headers) {
65215
+ const raw = getHeader(headers, "Content-ID");
65216
+ if (!raw) return null;
65217
+ const id = raw.replace(/^<+/, "").replace(/>+$/, "").trim().toLowerCase();
65218
+ return id || null;
65219
+ }
65220
+ function isEmbeddedResource(headers, htmlBodies) {
65221
+ if (!isInlineDisposition(headers)) return false;
65222
+ const cid = extractContentId(headers);
65223
+ if (!cid) return false;
65224
+ const needle = `cid:${cid}`;
65225
+ return htmlBodies.some((html) => html.includes(needle));
65226
+ }
65227
+ function collectHtmlBodies(parts) {
65228
+ const bodies = [];
65229
+ for (const part of parts) {
65230
+ if (extractMimeType(part.headers) !== "text/html") continue;
65231
+ const encoding = getHeader(part.headers, "Content-Transfer-Encoding");
65232
+ bodies.push(
65233
+ decodePartText(decodeBody(part.body, encoding), extractCharset(part.headers)).toLowerCase()
65234
+ );
65235
+ }
65236
+ return bodies;
65237
+ }
65214
65238
  function extractSize(headers) {
65215
65239
  const dispHeader = getHeader(headers, "Content-Disposition");
65216
65240
  if (dispHeader) {
@@ -65316,11 +65340,12 @@ function parseMimeAttachments(source) {
65316
65340
  const boundary = extractBoundary(source);
65317
65341
  if (!boundary) return [];
65318
65342
  const parts = walkLeafParts(source, boundary);
65343
+ const htmlBodies = collectHtmlBodies(parts);
65319
65344
  const attachments = [];
65320
65345
  for (const part of parts) {
65321
65346
  const filename = extractFilename(part.headers);
65322
65347
  if (!filename) continue;
65323
- if (isInlineDisposition(part.headers)) continue;
65348
+ if (isEmbeddedResource(part.headers, htmlBodies)) continue;
65324
65349
  const encoding = getHeader(part.headers, "Content-Transfer-Encoding");
65325
65350
  attachments.push({
65326
65351
  name: filename,
@@ -82916,6 +82941,49 @@ function preflightAttachments(attachments) {
82916
82941
  }
82917
82942
  }
82918
82943
 
82944
+ // src/utils/senderAccount.ts
82945
+ function extractAddress(sender) {
82946
+ const angle = /<([^<>]*)>\s*$/.exec(sender);
82947
+ return (angle ? angle[1] : sender).trim();
82948
+ }
82949
+ var same = (a, b) => a.trim().toLowerCase() === b.trim().toLowerCase();
82950
+ function describe(accounts) {
82951
+ const usable = accounts.filter((a) => a.enabled && a.addresses.length > 0);
82952
+ if (usable.length === 0) return "No enabled Mail account with an address was found.";
82953
+ return `Available: ${usable.map((a) => `${a.name} <${a.addresses.join(", ")}>`).join("; ")}`;
82954
+ }
82955
+ function pickSenderAddress(input, accounts) {
82956
+ const wanted = extractAddress(input);
82957
+ const enabled = accounts.filter((a) => a.enabled);
82958
+ const address = enabled.flatMap((a) => a.addresses).find((addr) => same(addr, wanted));
82959
+ if (address) return { address };
82960
+ const byName = enabled.filter((a) => same(a.name, input));
82961
+ if (byName.length > 1) {
82962
+ return {
82963
+ error: `Account name "${input}" matches ${byName.length} accounts; pass the exact sender address instead. ${describe(accounts)}`
82964
+ };
82965
+ }
82966
+ if (byName.length === 1) {
82967
+ const { addresses } = byName[0];
82968
+ if (addresses.length === 1) return { address: addresses[0] };
82969
+ return {
82970
+ error: `Account "${input}" has ${addresses.length === 0 ? "no address" : `${addresses.length} addresses`}; pass the exact sender address instead. ${describe(accounts)}`
82971
+ };
82972
+ }
82973
+ return {
82974
+ error: `No enabled Mail account or address matches "${input}", so nothing was sent or saved. ${describe(accounts)}`
82975
+ };
82976
+ }
82977
+ function senderGuardScript(safeAddress) {
82978
+ const check2 = safeAddress ? `
82979
+ if not (actualSender is "${safeAddress}" or actualSender ends with "<${safeAddress}>") then
82980
+ delete newMessage
82981
+ return "error:Mail.app would send as " & actualSender & " instead of ${safeAddress}, so nothing was sent or saved."
82982
+ end if` : "";
82983
+ return `
82984
+ set actualSender to sender of newMessage as text${check2}`;
82985
+ }
82986
+
82919
82987
  // src/services/appleMailManager.ts
82920
82988
  init_attachmentReadPolicy();
82921
82989
 
@@ -83123,6 +83191,9 @@ function extractFreshlyStalledMailboxes(raw) {
83123
83191
  var GROUP_SEP = "";
83124
83192
  var FIELD_SEP = "";
83125
83193
  var RECORD_SEP = "";
83194
+ var ITEM_SEP = "";
83195
+ var SENT_PREFIX = "sent:";
83196
+ var DRAFT_PREFIX = "draft created:";
83126
83197
  var DIAG_MARKER = "DIAG";
83127
83198
  var DIAG_FIELD_SEP = "F";
83128
83199
  var DIAG_ITEM_SEP = "M";
@@ -83499,6 +83570,13 @@ function looksLikeMailNotRunning(error3) {
83499
83570
  error3
83500
83571
  );
83501
83572
  }
83573
+ function parseComposeOutput(output, prefix) {
83574
+ const out = output.trim();
83575
+ if (out.startsWith(prefix)) return { success: true, from: out.slice(prefix.length) };
83576
+ if (out.startsWith("error:")) return { success: false, error: out.slice("error:".length) };
83577
+ console.error(`Unexpected compose output: ${out.slice(0, 200)}`);
83578
+ return { success: false };
83579
+ }
83502
83580
  function mailboxPathFragment(mailboxVar, outputVar) {
83503
83581
  return `
83504
83582
  set ${outputVar} to name of ${mailboxVar}
@@ -85546,8 +85624,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85546
85624
  * @param body - Email body (plain text)
85547
85625
  * @param cc - CC recipients
85548
85626
  * @param bcc - BCC recipients
85549
- * @param account - Account to send from
85550
- * @returns true if sent successfully
85627
+ * @param account - Account name or exact address to send from; resolved to one
85628
+ * address and verified before sending (see `utils/senderAccount.ts`)
85629
+ * @returns `from` is the sender Mail.app actually used
85551
85630
  */
85552
85631
  // ───────────────────────────────────────────────────────────────────
85553
85632
  // KNOWN BUG: outgoing emails sent via AppleScript on macOS 15+ get wrapped
@@ -85593,7 +85672,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85593
85672
  }
85594
85673
  const mat = materializeAttachments(attachments);
85595
85674
  try {
85596
- return this.sendEmailWithPaths(
85675
+ return this.composeWithCommands(
85676
+ "send",
85597
85677
  recipientCommands,
85598
85678
  safeSubject,
85599
85679
  safeBody,
@@ -85604,38 +85684,55 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85604
85684
  mat.cleanup();
85605
85685
  }
85606
85686
  }
85607
- sendEmailWithPaths(recipientCommands, safeSubject, safeBody, account, attachmentCommands) {
85608
- let sendCommand;
85609
- if (account) {
85610
- const safeAccount = escapeForAppleScript(account);
85611
- sendCommand = `
85612
- set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:true}
85613
- tell newMessage
85614
- ${recipientCommands}
85615
- set sender to "${safeAccount}"
85616
- ${attachmentCommands}
85617
- end tell
85618
- send newMessage
85619
- return "sent"
85620
- `;
85621
- } else {
85622
- sendCommand = `
85623
- set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:true}
85687
+ /**
85688
+ * Build and run the AppleScript compose for send-email (`send`) and
85689
+ * create-draft (`draft`): pin the sender when `account` is given, verify it
85690
+ * before the message leaves, and report the sender Mail actually used.
85691
+ */
85692
+ composeWithCommands(mode, recipientCommands, safeSubject, safeBody, account, attachmentCommands) {
85693
+ const sender = this.resolveSender(account);
85694
+ if ("error" in sender) return { success: false, error: sender.error };
85695
+ const prefix = mode === "send" ? SENT_PREFIX : DRAFT_PREFIX;
85696
+ const script = buildAppLevelScript(`
85697
+ set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:${mode === "send"}}
85624
85698
  tell newMessage
85625
85699
  ${recipientCommands}
85700
+ ${sender.setSender}
85626
85701
  ${attachmentCommands}
85627
85702
  end tell
85628
- send newMessage
85629
- return "sent"
85630
- `;
85631
- }
85632
- const script = buildAppLevelScript(sendCommand);
85703
+ ${sender.guard}
85704
+ ${mode === "send" ? "send newMessage" : ""}
85705
+ return "${prefix}" & actualSender
85706
+ `);
85633
85707
  const result = executeAppleScript(script, { timeoutMs: 6e4, maxRetries: 1 });
85634
85708
  if (!result.success) {
85635
- console.error(`Failed to send email: ${result.error}`);
85636
- return false;
85709
+ console.error(
85710
+ `Failed to ${mode === "send" ? "send email" : "create draft"}: ${result.error}`
85711
+ );
85712
+ return { success: false };
85637
85713
  }
85638
- return result.output.includes("sent");
85714
+ return parseComposeOutput(result.output, prefix);
85715
+ }
85716
+ /**
85717
+ * Turn the caller's `account` into the AppleScript pieces that pin the
85718
+ * sender and verify it. No `account` → no pin, but the script still reports
85719
+ * the sender Mail picked. Uses the cached account list: a stale list can only
85720
+ * turn a valid account into a refusal, and the in-script guard re-checks the
85721
+ * real sender before anything leaves.
85722
+ */
85723
+ resolveSender(account) {
85724
+ if (!account) return { setSender: "", guard: senderGuardScript(void 0) };
85725
+ const { accounts, failed } = this.listAccountsChecked({ timeoutMs: 3e4 });
85726
+ if (failed) {
85727
+ return { error: "Could not read Mail accounts to resolve the sender; nothing was sent." };
85728
+ }
85729
+ const picked = pickSenderAddress(account, accounts);
85730
+ if ("error" in picked) return { error: picked.error };
85731
+ const safeAddress = escapeForAppleScript(picked.address);
85732
+ return {
85733
+ setSender: `set sender to "${safeAddress}"`,
85734
+ guard: senderGuardScript(safeAddress)
85735
+ };
85639
85736
  }
85640
85737
  /**
85641
85738
  * Send individual personalized emails to a list of recipients (mail merge).
@@ -85653,6 +85750,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85653
85750
  sendSerialEmail(recipients, subject, body, account, delayMs = 500) {
85654
85751
  const effectiveDelay = Math.min(Math.max(delayMs, 0), 1e4);
85655
85752
  const results = [];
85753
+ const sender = this.resolveSender(account);
85754
+ if ("error" in sender) {
85755
+ return recipients.map((r) => ({ email: r.email, success: false, error: sender.error }));
85756
+ }
85656
85757
  for (let i = 0; i < recipients.length; i++) {
85657
85758
  const recipient = recipients[i];
85658
85759
  try {
@@ -85664,7 +85765,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85664
85765
  personalizedSubject = personalizedSubject.replace(placeholder, value);
85665
85766
  personalizedBody = personalizedBody.replace(placeholder, value);
85666
85767
  }
85667
- const success = this.sendEmail(
85768
+ const sent = this.sendEmail(
85668
85769
  [recipient.email],
85669
85770
  personalizedSubject,
85670
85771
  personalizedBody,
@@ -85674,8 +85775,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85674
85775
  );
85675
85776
  results.push({
85676
85777
  email: recipient.email,
85677
- success,
85678
- error: success ? void 0 : "Failed to send email"
85778
+ success: sent.success,
85779
+ error: sent.success ? void 0 : sent.error ?? "Failed to send email"
85679
85780
  });
85680
85781
  } catch (error3) {
85681
85782
  results.push({
@@ -85699,7 +85800,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85699
85800
  * @param cc - CC recipients
85700
85801
  * @param bcc - BCC recipients
85701
85802
  * @param account - Account to create draft in
85702
- * @returns true if draft created successfully
85803
+ * @returns `from` is the sender Mail.app actually used
85703
85804
  */
85704
85805
  createDraft(to, subject, body, cc, bcc, account, attachments) {
85705
85806
  const safeSubject = escapeForAppleScript(subject);
@@ -85724,7 +85825,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85724
85825
  const mat = materializeAttachments(attachments);
85725
85826
  const attachmentCommands = buildAttachmentCommands(mat.paths);
85726
85827
  try {
85727
- return this.createDraftWithCommands(
85828
+ return this.composeWithCommands(
85829
+ "draft",
85728
85830
  recipientCommands,
85729
85831
  safeSubject,
85730
85832
  safeBody,
@@ -85735,37 +85837,6 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85735
85837
  mat.cleanup();
85736
85838
  }
85737
85839
  }
85738
- createDraftWithCommands(recipientCommands, safeSubject, safeBody, account, attachmentCommands) {
85739
- let draftCommand;
85740
- if (account) {
85741
- const safeAccount = escapeForAppleScript(account);
85742
- draftCommand = `
85743
- set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:false}
85744
- tell newMessage
85745
- ${recipientCommands}
85746
- set sender to "${safeAccount}"
85747
- ${attachmentCommands}
85748
- end tell
85749
- return "draft created"
85750
- `;
85751
- } else {
85752
- draftCommand = `
85753
- set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:false}
85754
- tell newMessage
85755
- ${recipientCommands}
85756
- ${attachmentCommands}
85757
- end tell
85758
- return "draft created"
85759
- `;
85760
- }
85761
- const script = buildAppLevelScript(draftCommand);
85762
- const result = executeAppleScript(script, { timeoutMs: 6e4, maxRetries: 1 });
85763
- if (!result.success) {
85764
- console.error(`Failed to create draft: ${result.error}`);
85765
- return false;
85766
- }
85767
- return result.output.includes("draft created");
85768
- }
85769
85840
  /**
85770
85841
  * Reply to a message.
85771
85842
  *
@@ -86640,13 +86711,29 @@ ${this.errorEmit(" ")}
86640
86711
  set msg to item 1 of matchingMsgs
86641
86712
  set outputText to ""
86642
86713
  set attCount to 0
86714
+ -- Each property is read inside its own try: \`MIME type of att\` throws
86715
+ -- "AppleEvent handler failed" for attachments Apple Mail itself composed,
86716
+ -- and one unguarded throw discards the whole list, so the MIME fallback
86717
+ -- answers "no attachments". An unreadable type or size degrades to a
86718
+ -- default; only a missing name drops the entry (it cannot be addressed).
86643
86719
  repeat with att in mail attachments of msg
86644
- set attName to name of att
86645
- set attType to MIME type of att
86646
- set attSize to file size of att as string
86647
- if attCount > 0 then set outputText to outputText & "${RECORD_SEP}"
86648
- set outputText to outputText & attName & "${FIELD_SEP}" & attType & "${FIELD_SEP}" & attSize
86649
- set attCount to attCount + 1
86720
+ set attName to ""
86721
+ set attType to "application/octet-stream"
86722
+ set attSize to "0"
86723
+ try
86724
+ set attName to name of att
86725
+ end try
86726
+ try
86727
+ set attType to MIME type of att
86728
+ end try
86729
+ try
86730
+ set attSize to file size of att as string
86731
+ end try
86732
+ if attName is not "" then
86733
+ if attCount > 0 then set outputText to outputText & "${RECORD_SEP}"
86734
+ set outputText to outputText & attName & "${FIELD_SEP}" & attType & "${FIELD_SEP}" & attSize
86735
+ set attCount to attCount + 1
86736
+ end if
86650
86737
  end repeat
86651
86738
  return outputText
86652
86739
  end if
@@ -87496,12 +87583,9 @@ end tell`;
87496
87583
  set accountList to {}
87497
87584
  repeat with acct in accounts
87498
87585
  set acctName to name of acct
87499
- set acctEmail to email addresses of acct
87500
87586
  set acctEnabled to enabled of acct
87501
- set emailStr to ""
87502
- if (count of acctEmail) > 0 then
87503
- set emailStr to item 1 of acctEmail
87504
- end if
87587
+ set AppleScript's text item delimiters to "${ITEM_SEP}"
87588
+ set emailStr to (email addresses of acct) as text
87505
87589
  set end of accountList to acctName & "${FIELD_SEP}" & emailStr & "${FIELD_SEP}" & acctEnabled
87506
87590
  end repeat
87507
87591
  set AppleScript's text item delimiters to "${RECORD_SEP}"
@@ -87522,9 +87606,11 @@ end tell`;
87522
87606
  for (const item of items) {
87523
87607
  const parts = item.split(FIELD_SEP);
87524
87608
  if (parts.length < 3) continue;
87609
+ const addresses = parts[1].split(ITEM_SEP).filter(Boolean);
87525
87610
  accounts.push({
87526
87611
  name: parts[0],
87527
- email: parts[1],
87612
+ email: addresses[0] ?? "",
87613
+ addresses,
87528
87614
  enabled: parts[2] === "true"
87529
87615
  });
87530
87616
  }
@@ -87772,7 +87858,7 @@ ${actionStmts.join("\n")}
87772
87858
  const subject = overrides?.subject ?? template.subject;
87773
87859
  const body = overrides?.body ?? template.body;
87774
87860
  if (to.length === 0) return false;
87775
- return this.createDraft(to, subject, body, cc);
87861
+ return this.createDraft(to, subject, body, cc).success;
87776
87862
  }
87777
87863
  // ===========================================================================
87778
87864
  // Diagnostics
@@ -90175,6 +90261,9 @@ var SENT_COPY_SCHEMA = external_exports.boolean().optional().describe(
90175
90261
  "SMTP only: whether a best-effort copy was filed to the account's Sent mailbox over IMAP (issue #220). Absent when no configured IMAP account matches the SMTP identity \u2014 not a failure, the feature simply wasn't engaged."
90176
90262
  );
90177
90263
  var SENT_COPY_ERROR_SCHEMA = external_exports.string().optional().describe("Present only when sentCopy is false: why the Sent-folder copy failed.");
90264
+ var SEND_ACCOUNT_SCHEMA = external_exports.string().optional().describe(
90265
+ "Account to send from. AppleScript transport: a Mail account name or an exact sender address, resolved to one address and verified before sending; if it matches no enabled account, or Mail would use a different sender, nothing is sent and an error is returned. Omitted \u2192 Mail's default sender (reported back in `from`). SMTP transport: an email-form From override (see README)."
90266
+ );
90178
90267
  registerTool(
90179
90268
  "send-email",
90180
90269
  {
@@ -90185,7 +90274,7 @@ registerTool(
90185
90274
  body: external_exports.string().min(1, "Body is required"),
90186
90275
  cc: external_exports.array(external_exports.string()).optional().describe("CC recipients"),
90187
90276
  bcc: external_exports.array(external_exports.string()).optional().describe("BCC recipients"),
90188
- account: external_exports.string().optional().describe("Account to send from"),
90277
+ account: SEND_ACCOUNT_SCHEMA,
90189
90278
  attachments: ATTACHMENTS_SCHEMA,
90190
90279
  inReplyTo: IN_REPLY_TO_SCHEMA,
90191
90280
  references: REFERENCES_SCHEMA,
@@ -90201,6 +90290,7 @@ registerTool(
90201
90290
  recipients: external_exports.array(external_exports.string()).optional(),
90202
90291
  attachmentCount: external_exports.number().optional(),
90203
90292
  transport: external_exports.string().optional(),
90293
+ from: external_exports.string().optional().describe("AppleScript transport: the sender Mail.app used"),
90204
90294
  messageId: external_exports.string().optional(),
90205
90295
  sentCopy: SENT_COPY_SCHEMA,
90206
90296
  sentCopyError: SENT_COPY_ERROR_SCHEMA
@@ -90252,16 +90342,20 @@ registerTool(
90252
90342
  ...result.sentCopyError !== void 0 ? { sentCopyError: result.sentCopyError } : {}
90253
90343
  });
90254
90344
  }
90255
- const success = mailManager.sendEmail(to, subject, body, cc, bcc, account, attachments);
90256
- if (!success) {
90257
- return errorResponse("Failed to send email. Check Mail.app configuration.");
90345
+ const sent = mailManager.sendEmail(to, subject, body, cc, bcc, account, attachments);
90346
+ if (!sent.success) {
90347
+ return errorResponse(sent.error ?? "Failed to send email. Check Mail.app configuration.");
90258
90348
  }
90259
- return successResponse(`Email sent to ${to.join(", ")}${attachInfo}`, {
90260
- ok: true,
90261
- recipients: to,
90262
- attachmentCount,
90263
- transport: "applescript"
90264
- });
90349
+ return successResponse(
90350
+ `Email sent to ${to.join(", ")}${attachInfo}${sent.from ? ` from ${sent.from}` : ""}`,
90351
+ {
90352
+ ok: true,
90353
+ recipients: to,
90354
+ attachmentCount,
90355
+ transport: "applescript",
90356
+ from: sent.from
90357
+ }
90358
+ );
90265
90359
  },
90266
90360
  "Error sending email"
90267
90361
  )
@@ -90279,7 +90373,7 @@ registerTool(
90279
90373
  ).min(1, "At least one recipient is required").max(100, "Cannot send to more than 100 recipients in a single batch").describe("List of recipients with personalization variables (max 100)"),
90280
90374
  subject: external_exports.string().min(1, "Subject is required").describe("Subject line \u2014 use {{Key}} for placeholders"),
90281
90375
  body: external_exports.string().min(1, "Body is required").describe("Email body \u2014 use {{Key}} for placeholders"),
90282
- account: external_exports.string().optional().describe("Account to send from"),
90376
+ account: SEND_ACCOUNT_SCHEMA,
90283
90377
  delayMs: external_exports.number().min(0).max(1e4).optional().describe("Delay between sends in ms (default: 500, max: 10000)")
90284
90378
  },
90285
90379
  outputSchema: {
@@ -90333,7 +90427,7 @@ registerTool(
90333
90427
  cc: external_exports.array(external_exports.string()).optional().describe("CC recipients"),
90334
90428
  bcc: external_exports.array(external_exports.string()).optional().describe("BCC recipients"),
90335
90429
  account: external_exports.string().optional().describe(
90336
- "Account to create draft in. With inReplyTo/references this must name a configured IMAP account (label or login); when omitted, the IMAP account matching the SMTP identity, else the only IMAP account, is used."
90430
+ "Account to create draft in: a Mail account name or an exact sender address, resolved and verified before the draft is saved (no match or a different sender \u2192 error, no draft). With inReplyTo/references this must name a configured IMAP account (label or login); when omitted, the IMAP account matching the SMTP identity, else the only IMAP account, is used."
90337
90431
  ),
90338
90432
  attachments: ATTACHMENTS_SCHEMA,
90339
90433
  inReplyTo: IN_REPLY_TO_SCHEMA,
@@ -90344,6 +90438,7 @@ registerTool(
90344
90438
  recipients: external_exports.array(external_exports.string()).optional(),
90345
90439
  attachmentCount: external_exports.number().optional(),
90346
90440
  transport: external_exports.enum(["applescript", "imap"]).optional(),
90441
+ from: external_exports.string().optional().describe("AppleScript draft: the sender Mail.app used"),
90347
90442
  account: external_exports.string().optional().describe("Threaded draft only: IMAP account it was filed in"),
90348
90443
  mailbox: external_exports.string().optional().describe("Threaded draft only: Drafts mailbox path"),
90349
90444
  messageId: external_exports.string().optional().describe("Threaded draft only: the draft's Message-ID"),
@@ -90366,18 +90461,24 @@ registerTool(
90366
90461
  references
90367
90462
  });
90368
90463
  }
90369
- const success = mailManager.createDraft(to, subject, body, cc, bcc, account, attachments);
90370
- if (!success) {
90371
- return errorResponse("Failed to create draft. Check Mail.app configuration.");
90464
+ const drafted = mailManager.createDraft(to, subject, body, cc, bcc, account, attachments);
90465
+ if (!drafted.success) {
90466
+ return errorResponse(
90467
+ drafted.error ?? "Failed to create draft. Check Mail.app configuration."
90468
+ );
90372
90469
  }
90373
90470
  const attachmentCount = attachments?.length ?? 0;
90374
90471
  const attachInfo = attachmentCount ? ` with ${attachmentCount} attachment(s)` : "";
90375
- return successResponse(`Draft created for ${to.join(", ")}${attachInfo}`, {
90376
- ok: true,
90377
- recipients: to,
90378
- attachmentCount,
90379
- transport: "applescript"
90380
- });
90472
+ return successResponse(
90473
+ `Draft created for ${to.join(", ")}${attachInfo}${drafted.from ? ` from ${drafted.from}` : ""}`,
90474
+ {
90475
+ ok: true,
90476
+ recipients: to,
90477
+ attachmentCount,
90478
+ transport: "applescript",
90479
+ from: drafted.from
90480
+ }
90481
+ );
90381
90482
  },
90382
90483
  "Error creating draft"
90383
90484
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.20.12",
3
+ "version": "2.20.14",
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",