apple-mail-mcp 2.20.14 → 2.20.16

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 +33 -28
  2. package/build/index.js +74 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -227,21 +227,21 @@ This section documents all available tools. AI agents should use these tool name
227
227
 
228
228
  Search for messages matching criteria. Searches all accounts by default.
229
229
 
230
- | Parameter | Type | Required | Description |
231
- | ----------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
232
- | `query` | string | No | Text to search in subject/sender |
233
- | `body` | string | No | Text to search in the message body. IMAP backend only (server-side `BODY` search); AppleScript-only accounts are reported as not searched |
234
- | `from` | string | No | Filter by sender email address |
235
- | `subject` | string | No | Filter by subject line |
236
- | `mailbox` | string | No | Mailbox to search in (omit to search all mailboxes) |
237
- | `account` | string | No | Account to search in (omit to search all accounts) |
238
- | `isRead` | boolean | No | Filter by read status |
239
- | `isFlagged` | boolean | No | Filter by flagged status |
230
+ | Parameter | Type | Required | Description |
231
+ | ----------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
232
+ | `query` | string | No | Text to search in subject/sender |
233
+ | `body` | string | No | Text to search in the message body. IMAP backend only (server-side `BODY` search); AppleScript-only accounts are reported as not searched |
234
+ | `from` | string | No | Filter by sender email address |
235
+ | `subject` | string | No | Filter by subject line |
236
+ | `mailbox` | string | No | Mailbox to search in (omit to search all mailboxes) |
237
+ | `account` | string | No | Account to search in (omit to search all accounts) |
238
+ | `isRead` | boolean | No | Filter by read status |
239
+ | `isFlagged` | boolean | No | Filter by flagged status |
240
240
  | `flagColor` | string | No | Filter to messages flagged with exactly this color — see [`flag-message`](#flag-message--unflag-message) for the palette. Implies `isFlagged: true`; pairing this with `isFlagged: false` is rejected. See the flag-color note below. |
241
- | `dateFrom` | string | No | Start date filter (e.g., "January 1, 2026") |
242
- | `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
243
- | `limit` | number | No | Max results, 1–500 (default: 50) |
244
- | `offset` | number | No | Newest matches to skip, ≥ 0 (for pagination). IMAP accounts only; without a `mailbox`, `offset` + `limit` may not exceed 5,000 |
241
+ | `dateFrom` | string | No | Start date filter (e.g., "January 1, 2026") |
242
+ | `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
243
+ | `limit` | number | No | Max results, 1–500 (default: 50) |
244
+ | `offset` | number | No | Newest matches to skip, ≥ 0 (for pagination). IMAP accounts only; without a `mailbox`, `offset` + `limit` may not exceed 5,000 |
245
245
 
246
246
  **Returns:** List of matching messages with ID, date, subject, sender, read state,
247
247
  and `flagColor` when flagged with an explicit color.
@@ -436,19 +436,19 @@ Send a new email immediately.
436
436
 
437
437
  **⚠️ Safety:** Sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling.
438
438
 
439
- | Parameter | Type | Required | Description |
440
- | ------------- | --------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
441
- | `to` | string[] | Yes | Recipient addresses |
442
- | `subject` | string | Yes | Email subject |
443
- | `body` | string | Yes | Email body (plain text) |
444
- | `cc` | string[] | No | CC recipients |
445
- | `bcc` | string[] | No | BCC recipients |
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
- | `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
- | `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
- | `replyTo` | string | No | SMTP only: `Reply-To` header, when replies should go somewhere other than the From address. Ignored on the AppleScript transport |
450
- | `inReplyTo` | string | No | SMTP only (2.20.0): thread the message as a reply — the parent's `Message-ID` token with angle brackets, e.g. `"<abc@mail.example.com>"`. Emitted verbatim, never RFC 2047-encoded. See [Threading headers](#threading-headers-inreplyto--references) |
451
- | `references` | string[] | No | SMTP only (2.20.0): the `References` chain, oldest first (the parent's own `References` + its `Message-ID`). Defaults to `[inReplyTo]` when omitted |
439
+ | Parameter | Type | Required | Description |
440
+ | ------------- | --------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
441
+ | `to` | string[] | Yes | Recipient addresses |
442
+ | `subject` | string | Yes | Email subject |
443
+ | `body` | string | Yes | Email body (plain text) |
444
+ | `cc` | string[] | No | CC recipients |
445
+ | `bcc` | string[] | No | BCC recipients |
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
+ | `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
+ | `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
+ | `replyTo` | string | No | SMTP only: `Reply-To` header, when replies should go somewhere other than the From address. Ignored on the AppleScript transport |
450
+ | `inReplyTo` | string | No | SMTP only (2.20.0): thread the message as a reply — the parent's `Message-ID` token with angle brackets, e.g. `"<abc@mail.example.com>"`. Emitted verbatim, never RFC 2047-encoded. See [Threading headers](#threading-headers-inreplyto--references) |
451
+ | `references` | string[] | No | SMTP only (2.20.0): the `References` chain, oldest first (the parent's own `References` + its `Message-ID`). Defaults to `[inReplyTo]` when omitted |
452
452
 
453
453
  **Example:**
454
454
 
@@ -954,12 +954,17 @@ nodemailer path `send-email` uses, so the Sent-folder copy (`sentCopy`) carries
954
954
  On the AppleScript transport they are added with `make new attachment` **after** the
955
955
  reply content is set and before `save`/`send` (inline items are written to `0600` temp
956
956
  files and removed afterwards), so a `send: false` draft contains both the attachments and
957
- the quoted original. Results report `attachmentCount`.
957
+ the quoted original. `make new attachment` returns before Mail has loaded the file, so the
958
+ script waits until Mail counts every file (bounded at 15 s) before saving or sending
959
+ (2.20.16, #293; the same wait applies to `send-email` and `create-draft`). Results report
960
+ `attachmentCount`.
958
961
 
959
962
  **Delivery:** `imap:` IDs are read directly from their encoded account, mailbox, and UID. Numeric IDs are read through Mail.app. With SMTP configured, replies use clean MIME with the original `Message-ID` in `In-Reply-To` and the full `References` chain. Only the original plain-text body is quoted; the new text is not. Pass `transport: "smtp"` to require this path, or `transport: "applescript"` to use Mail.app explicitly.
960
963
 
961
964
  **AppleScript transport quoting (v2.19.12):** every draft (`send: false`) and any send that falls back to AppleScript (SMTP not configured) also gets the original quoted — an "On \<date\>, \<sender\> wrote:" attribution line followed by the `> `-prefixed original body, built the same way as the SMTP path. Mail's own `reply`/`forward` AppleScript commands generate this quote internally but expose no way to read it back, so it's built here instead of relying on Mail's (unreadable) copy. Unlike SMTP, this path has no sending-identity restriction — it's read-only source lookup, not sending.
962
965
 
966
+ **Sender on the AppleScript transport (2.20.15, #294):** Mail's `reply`/`forward` commands set no sender, so Mail would answer from its default sending account. The reply or forward is now sent from the account the original sits in: the original's to/cc address that belongs to that account — or, for a message you sent, its own From address — else the account's first address. The sender is read back before saving or sending; a mismatch deletes the compose and returns an error. A message in an "On My Mac" mailbox has no account, so its recipients are matched against every enabled account and, with no match, the sender is left to Mail. The same applies to `forward-message`.
967
+
963
968
  **Failure behavior:** once SMTP is selected, a missing password, unreadable source, missing reply address or `Message-ID`, or SMTP failure returns an error. It does not silently switch to AppleScript or create an unthreaded new message. With SMTP unconfigured and transport omitted, AppleScript remains available. `send: false` saves a Mail.app draft; combining it with `transport: "smtp"` is rejected before composing.
964
969
 
965
970
  The direct IMAP source read is bounded to 25 MiB, including MIME attachments. Its account login must match the SMTP login, configured From, or an explicitly configured `APPLE_MAIL_MCP_SMTP_ALLOWED_FROM` identity; otherwise the call fails rather than sending from an unrelated account. The SMTP configuration still represents a single sending identity, not a per-account SMTP registry.
package/build/index.js CHANGED
@@ -83216,6 +83216,50 @@ function stripStreamDelimiters(value) {
83216
83216
  }
83217
83217
  var AMBIGUOUS_ID_PREFIX = "Message id ";
83218
83218
  var AMBIGUOUS_ID_BATCH = "This message id is present in more than one mailbox ";
83219
+ var senderFragment = (compose, verb) => `set _rsAddrs to {}
83220
+ set _rsOwn to true
83221
+ try
83222
+ set _rsAddrs to email addresses of (account of (mailbox of msg))
83223
+ set _rsN to count of _rsAddrs
83224
+ on error
83225
+ set _rsOwn to false
83226
+ set _rsAddrs to {}
83227
+ repeat with _rsAc in (every account whose enabled is true)
83228
+ try
83229
+ set _rsAddrs to _rsAddrs & (email addresses of _rsAc)
83230
+ end try
83231
+ end repeat
83232
+ end try
83233
+ if _rsOwn and (count of _rsAddrs) is 0 then error "The account this message arrived in has no email address to ${verb} from."
83234
+ set _rsFrom to ""
83235
+ if _rsOwn then set _rsFrom to (item 1 of _rsAddrs) as text
83236
+ set _rsCands to {}
83237
+ repeat with _rsR in ((to recipients of msg) & (cc recipients of msg))
83238
+ set end of _rsCands to (address of _rsR) as text
83239
+ end repeat
83240
+ try
83241
+ set end of _rsCands to (extract address from (sender of msg)) as text
83242
+ end try
83243
+ set _rsDone to false
83244
+ repeat with _rsC in _rsCands
83245
+ if not _rsDone then
83246
+ repeat with _rsA in _rsAddrs
83247
+ if (_rsC as text) is (_rsA as text) then
83248
+ set _rsFrom to (_rsA as text)
83249
+ set _rsDone to true
83250
+ exit repeat
83251
+ end if
83252
+ end repeat
83253
+ end if
83254
+ end repeat
83255
+ if _rsFrom is not "" then
83256
+ set sender of ${compose} to _rsFrom
83257
+ set _rsGot to sender of ${compose} as text
83258
+ if not (_rsGot is _rsFrom or _rsGot ends with "<" & _rsFrom & ">") then
83259
+ delete ${compose}
83260
+ error "Mail.app would ${verb} as " & _rsGot & " instead of " & _rsFrom & ", so nothing was sent or saved."
83261
+ end if
83262
+ end if`;
83219
83263
  function normalizeRfcMessageId(mid) {
83220
83264
  return (mid || "").trim().replace(/^<+/, "").replace(/>+$/, "").trim();
83221
83265
  }
@@ -83330,10 +83374,30 @@ function buildAttachmentCommands(attachments) {
83330
83374
  }
83331
83375
  return commands;
83332
83376
  }
83377
+ var ATTACHMENT_SETTLE_TIMEOUT_S = 15;
83378
+ var ATTACHMENT_SETTLE_GRACE_S = 1;
83379
+ function attachmentSettleBlock(msgVar, expected) {
83380
+ if (expected <= 0) return "";
83381
+ return `
83382
+ set attachDeadline to (current date) + ${ATTACHMENT_SETTLE_TIMEOUT_S}
83383
+ repeat
83384
+ set attachCount to 0
83385
+ try
83386
+ set attachCount to count of attachments of content of ${msgVar}
83387
+ end try
83388
+ if attachCount >= ${expected} then exit repeat
83389
+ if (current date) > attachDeadline then exit repeat
83390
+ delay 0.25
83391
+ end repeat
83392
+ delay ${ATTACHMENT_SETTLE_GRACE_S}`;
83393
+ }
83394
+ function countAttachmentCommands(commands) {
83395
+ return (commands.match(/make new attachment/g) || []).length;
83396
+ }
83333
83397
  function composeAttachmentBlock(msgVar, paths) {
83334
83398
  const commands = buildAttachmentCommands(paths);
83335
83399
  return commands ? `tell ${msgVar}
83336
- ${commands}end tell` : "";
83400
+ ${commands}end tell${attachmentSettleBlock(msgVar, paths.length)}` : "";
83337
83401
  }
83338
83402
  function asDateToString(varName) {
83339
83403
  return `((year of ${varName}) as string) & "-" & ((month of ${varName} as integer) as string) & "-" & ((day of ${varName}) as string) & "-" & ((hours of ${varName}) as string) & "-" & ((minutes of ${varName}) as string) & "-" & ((seconds of ${varName}) as string)`;
@@ -85686,12 +85750,15 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85686
85750
  }
85687
85751
  /**
85688
85752
  * 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.
85753
+ * create-draft (`draft`): pin the sender when `account` is given, wait for
85754
+ * any attachments to load (attachmentSettleBlock), verify the sender before
85755
+ * the message leaves, and report the sender Mail actually used. A draft is
85756
+ * saved and closed explicitly rather than left to Mail's autosave (#293).
85691
85757
  */
85692
85758
  composeWithCommands(mode, recipientCommands, safeSubject, safeBody, account, attachmentCommands) {
85693
85759
  const sender = this.resolveSender(account);
85694
85760
  if ("error" in sender) return { success: false, error: sender.error };
85761
+ const settle2 = attachmentSettleBlock("newMessage", countAttachmentCommands(attachmentCommands));
85695
85762
  const prefix = mode === "send" ? SENT_PREFIX : DRAFT_PREFIX;
85696
85763
  const script = buildAppLevelScript(`
85697
85764
  set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:${mode === "send"}}
@@ -85699,9 +85766,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85699
85766
  ${recipientCommands}
85700
85767
  ${sender.setSender}
85701
85768
  ${attachmentCommands}
85702
- end tell
85769
+ end tell${settle2}
85703
85770
  ${sender.guard}
85704
- ${mode === "send" ? "send newMessage" : ""}
85771
+ ${mode === "send" ? "send newMessage" : "save newMessage\n close newMessage saving yes"}
85705
85772
  return "${prefix}" & actualSender
85706
85773
  `);
85707
85774
  const result = executeAppleScript(script, { timeoutMs: 6e4, maxRetries: 1 });
@@ -85858,6 +85925,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85858
85925
  id,
85859
85926
  `
85860
85927
  set theReply to reply msg without opening window${replyAllClause}
85928
+ ${senderFragment("theReply", "reply")}
85861
85929
  set content of theReply to "${safeBody}"
85862
85930
  ${composeAttachmentBlock("theReply", mat.paths)}
85863
85931
  ${finalAction}`
@@ -85891,6 +85959,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85891
85959
  id,
85892
85960
  `
85893
85961
  set theForward to forward msg without opening window
85962
+ ${senderFragment("theForward", "forward")}
85894
85963
  ${recipientCommands}
85895
85964
  ${safeBody ? `set content of theForward to "${safeBody}"` : ""}
85896
85965
  ${composeAttachmentBlock("theForward", mat.paths)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.20.14",
3
+ "version": "2.20.16",
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",