apple-mail-mcp 2.19.10 → 2.19.12

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
@@ -826,7 +826,7 @@ Reply to an existing message.
826
826
  | Parameter | Type | Required | Description |
827
827
  | ----------- | ------- | -------- | ---------------------------------------------------------------------------------------------- |
828
828
  | `id` | string | Yes | Message ID to reply to |
829
- | `body` | string | Yes | Reply body |
829
+ | `body` | string | Yes | Reply body (plain text; HTML tags such as `<br>` are not rendered) |
830
830
  | `replyAll` | boolean | No | Reply to all recipients (default: false) |
831
831
  | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
832
832
  | `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
@@ -853,6 +853,8 @@ Reply to an existing message.
853
853
 
854
854
  **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.
855
855
 
856
+ **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.
857
+
856
858
  **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.
857
859
 
858
860
  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.
@@ -871,11 +873,11 @@ Forward a message to new recipients.
871
873
  | ----------- | -------- | -------- | ---------------------------------------------------------------------------------------------- |
872
874
  | `id` | string | Yes | Message ID to forward |
873
875
  | `to` | string[] | Yes | Recipients to forward to |
874
- | `body` | string | No | Message to prepend |
876
+ | `body` | string | No | Message to prepend (plain text) |
875
877
  | `send` | boolean | No | Send immediately (default: true, false = save as draft) |
876
878
  | `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
877
879
 
878
- **Delivery:** uses the same source lookup, transport selection, 25 MiB source limit, account-identity check, and failure behavior as `reply-to-message`. A forward deliberately starts a new conversation, so it has no `In-Reply-To` or `References` headers. The existing plain-text forwarding behavior is unchanged: original attachments are not reattached. See [SMTP transport](#smtp-transport).
880
+ **Delivery:** uses the same source lookup, transport selection, 25 MiB source limit, account-identity check, and failure behavior as `reply-to-message`. A forward deliberately starts a new conversation, so it has no `In-Reply-To` or `References` headers. The existing plain-text forwarding behavior is unchanged: original attachments are not reattached. See [SMTP transport](#smtp-transport). On the AppleScript transport, a `body` is merged with the forwarded-message header block and original text the same way the SMTP path builds it (see `reply-to-message`'s AppleScript transport quoting note); with no `body`, Mail's own forward content is left untouched.
879
881
 
880
882
  SMTP forwarding requires a readable plain-text original. HTML-only IMAP messages and failed Mail.app body reads return an error before sending instead of silently omitting the original content. Explicitly select `transport: "applescript"` to forward these with Mail.app; no automatic fallback occurs. An intentionally empty plain-text message is still valid.
881
883
 
package/build/cli.js CHANGED
@@ -58409,6 +58409,12 @@ async function fetchMailboxMatches(client, path, criteria, newestCount) {
58409
58409
  const found = await client.search(criteria, { uid: true });
58410
58410
  const uids = Array.isArray(found) ? found : [];
58411
58411
  if (uids.length === 0 || newestCount === 0) return { messages: [], total: uids.length };
58412
+ const status = await client.status(path, { messages: true });
58413
+ if (typeof status.messages === "number" && uids.length > status.messages) {
58414
+ throw new Error(
58415
+ `IMAP SEARCH on "${path}" reported ${uids.length} matches, more than the mailbox's own ${status.messages} messages \u2014 discarding as corrupted rather than trusting it (see #246).`
58416
+ );
58417
+ }
58412
58418
  const newest = uids.slice().reverse().slice(0, newestCount);
58413
58419
  const byUid = /* @__PURE__ */ new Map();
58414
58420
  for await (const msg of client.fetch(
package/build/index.js CHANGED
@@ -65738,6 +65738,12 @@ async function fetchMailboxMatches(client, path, criteria, newestCount) {
65738
65738
  const found = await client.search(criteria, { uid: true });
65739
65739
  const uids = Array.isArray(found) ? found : [];
65740
65740
  if (uids.length === 0 || newestCount === 0) return { messages: [], total: uids.length };
65741
+ const status = await client.status(path, { messages: true });
65742
+ if (typeof status.messages === "number" && uids.length > status.messages) {
65743
+ throw new Error(
65744
+ `IMAP SEARCH on "${path}" reported ${uids.length} matches, more than the mailbox's own ${status.messages} messages \u2014 discarding as corrupted rather than trusting it (see #246).`
65745
+ );
65746
+ }
65741
65747
  const newest = uids.slice().reverse().slice(0, newestCount);
65742
65748
  const byUid = /* @__PURE__ */ new Map();
65743
65749
  for await (const msg of client.fetch(
@@ -86841,10 +86847,6 @@ function buildReplyOptions(args) {
86841
86847
  );
86842
86848
  cc = extra.length ? extra : void 0;
86843
86849
  }
86844
- const attribution = buildAttribution(original);
86845
- const quoted = originalPlainText.trim() ? `
86846
-
86847
- ${attribution}${quoteBody(originalPlainText)}` : "";
86848
86850
  const references = dedupe(
86849
86851
  original.messageId ? [...original.references, original.messageId] : original.references
86850
86852
  );
@@ -86852,12 +86854,19 @@ ${attribution}${quoteBody(originalPlainText)}` : "";
86852
86854
  to,
86853
86855
  cc,
86854
86856
  subject: withSubjectPrefix(original.subject, "Re:"),
86855
- body: `${body}${quoted}`,
86857
+ body: buildReplyBody(body, original, originalPlainText),
86856
86858
  inReplyTo: original.messageId,
86857
86859
  references: references.length ? references : void 0,
86858
86860
  from
86859
86861
  };
86860
86862
  }
86863
+ function buildReplyBody(body, original, originalPlainText) {
86864
+ const attribution = buildAttribution(original);
86865
+ const quoted = originalPlainText.trim() ? `
86866
+
86867
+ ${attribution}${quoteBody(originalPlainText)}` : "";
86868
+ return `${body}${quoted}`;
86869
+ }
86861
86870
  function buildAttribution(original) {
86862
86871
  const who = original.from[0] ?? original.replyTo[0] ?? "the sender";
86863
86872
  return original.date ? `On ${original.date}, ${who} wrote:
@@ -86866,6 +86875,14 @@ function buildAttribution(original) {
86866
86875
  }
86867
86876
  function buildForwardOptions(args) {
86868
86877
  const { original, originalPlainText, to, body, from } = args;
86878
+ return {
86879
+ to: dedupe(to),
86880
+ subject: withSubjectPrefix(original.subject, "Fwd:"),
86881
+ body: buildForwardBody(original, originalPlainText, body),
86882
+ from
86883
+ };
86884
+ }
86885
+ function buildForwardBody(original, originalPlainText, body) {
86869
86886
  const headerBlock = [
86870
86887
  "---------- Forwarded message ----------",
86871
86888
  original.from.length ? `From: ${original.from.join(", ")}` : "",
@@ -86877,14 +86894,9 @@ function buildForwardOptions(args) {
86877
86894
  const prefix = body?.trim() ? `${body}
86878
86895
 
86879
86896
  ` : "";
86880
- return {
86881
- to: dedupe(to),
86882
- subject: withSubjectPrefix(original.subject, "Fwd:"),
86883
- body: `${prefix}${headerBlock}
86897
+ return `${prefix}${headerBlock}
86884
86898
 
86885
- ${originalPlainText}`,
86886
- from
86887
- };
86899
+ ${originalPlainText}`;
86888
86900
  }
86889
86901
 
86890
86902
  // src/tools/compose.ts
@@ -87022,6 +87034,31 @@ async function readOriginal(deps, id, cfg) {
87022
87034
  const content = deps.mail.getMessageContent(id);
87023
87035
  return { original: parseOriginalHeaders(raw), plainText: content?.plainText ?? null };
87024
87036
  }
87037
+ async function readOriginalForQuote(deps, id) {
87038
+ if (decodeImapId(id)) {
87039
+ try {
87040
+ const source = await deps.imapSource(id);
87041
+ const original = parseOriginalHeaders(source.raw);
87042
+ if (source.subject !== void 0) original.subject = source.subject;
87043
+ return { original, plainText: extractTextBody(source.raw) ?? "" };
87044
+ } catch {
87045
+ return null;
87046
+ }
87047
+ }
87048
+ const raw = deps.mail.getRawSource(id);
87049
+ if (!raw) return null;
87050
+ const content = deps.mail.getMessageContent(id);
87051
+ return { original: parseOriginalHeaders(raw), plainText: content?.plainText ?? "" };
87052
+ }
87053
+ async function replyComposeBody(deps, id, body) {
87054
+ const source = await readOriginalForQuote(deps, id);
87055
+ return source ? buildReplyBody(body, source.original, source.plainText) : body;
87056
+ }
87057
+ async function forwardComposeBody(deps, id, body) {
87058
+ if (!body) return body;
87059
+ const source = await readOriginalForQuote(deps, id);
87060
+ return source ? buildForwardBody(source.original, source.plainText, body) : body;
87061
+ }
87025
87062
  async function runCompose(deps, args) {
87026
87063
  const { id, send, transport: transport2 } = args;
87027
87064
  const verb = args.kind === "reply" ? "reply to" : "forward";
@@ -87092,7 +87129,17 @@ async function runCompose(deps, args) {
87092
87129
  return errorResponse(
87093
87130
  `Failed to ${verb} message "${id}": ${resolved.error ?? "message not found"}`
87094
87131
  );
87095
- const outcome = args.kind === "reply" ? deps.mail.replyToMessage(resolved.numericId, args.body, args.replyAll, send) : deps.mail.forwardMessage(resolved.numericId, args.to, args.body, send);
87132
+ const outcome = args.kind === "reply" ? deps.mail.replyToMessage(
87133
+ resolved.numericId,
87134
+ await replyComposeBody(deps, id, args.body),
87135
+ args.replyAll,
87136
+ send
87137
+ ) : deps.mail.forwardMessage(
87138
+ resolved.numericId,
87139
+ args.to,
87140
+ await forwardComposeBody(deps, id, args.body),
87141
+ send
87142
+ );
87096
87143
  if (!outcome.success)
87097
87144
  return errorResponse(
87098
87145
  `Failed to ${verb} message "${id}": ${outcome.error ?? "Mail.app compose failed"}`
@@ -88852,7 +88899,7 @@ registerTool(
88852
88899
  inputSchema: {
88853
88900
  id: MESSAGE_ID_SCHEMA,
88854
88901
  transport: COMPOSE_TRANSPORT_SCHEMA,
88855
- body: external_exports.string().min(1, "Reply body is required"),
88902
+ body: external_exports.string().min(1, "Reply body is required").describe("Reply body (plain text; HTML tags such as <br> are not rendered)"),
88856
88903
  replyAll: external_exports.boolean().optional().default(false).describe("Reply to all recipients"),
88857
88904
  send: external_exports.boolean().optional().default(true).describe("Send immediately (false = save as draft)")
88858
88905
  },
@@ -88876,7 +88923,7 @@ registerTool(
88876
88923
  id: MESSAGE_ID_SCHEMA,
88877
88924
  transport: COMPOSE_TRANSPORT_SCHEMA,
88878
88925
  to: external_exports.array(external_exports.string()).min(1, "At least one recipient is required"),
88879
- body: external_exports.string().optional().describe("Optional message to prepend"),
88926
+ body: external_exports.string().optional().describe("Optional message to prepend (plain text)"),
88880
88927
  send: external_exports.boolean().optional().default(true).describe("Send immediately (false = save as draft)")
88881
88928
  },
88882
88929
  outputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.19.10",
3
+ "version": "2.19.12",
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",