apple-mail-mcp 2.8.15 → 2.9.0

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 +61 -0
  2. package/build/index.js +516 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -879,6 +879,67 @@ Rename a mailbox (creates new, moves messages, deletes old).
879
879
 
880
880
  ---
881
881
 
882
+ ### Smart Mailbox Operations (intelligente Postfächer)
883
+
884
+ Smart mailboxes are Apple Mail's **criteria-based virtual views** — not real folders, so no messages are moved. AppleScript's `smart mailbox` / `intelligentes Postfach` terms don't compile reliably on localized (e.g. German) macOS, so these tools read and edit `~/Library/Mail/V*/MailData/SyncedSmartMailboxes.plist` directly.
885
+
886
+ **How writes stay safe:** creating or deleting a smart mailbox first backs the plist up to `SyncedSmartMailboxes.plist.bak`, edits a temp copy with `plutil`/`PlistBuddy`, validates it with `plutil -lint`, and only then atomically renames it into place. Your **existing** smart mailboxes — including any with date/data criteria — are never rewritten, only the single target entry is added or removed. These tools do **not** quit or restart Mail: **quit Mail first** for reliable results, since a running Mail may not show a new smart mailbox until it's relaunched and can overwrite plist edits it didn't make.
887
+
888
+ #### `list-smart-mailboxes`
889
+
890
+ List existing smart mailboxes.
891
+
892
+ **Parameters:** None
893
+
894
+ **Returns:** List of smart mailbox names + criteria summary.
895
+
896
+ ---
897
+
898
+ #### `create-smart-mailbox`
899
+
900
+ Create a smart mailbox with a simple contains rule.
901
+
902
+ | Parameter | Type | Required | Description |
903
+ |-----------|------|----------|-------------|
904
+ | `name` | string | Yes | Name for the smart mailbox |
905
+ | `fromContains` | string | No | Match if From contains this |
906
+ | `subjectContains` | string | No | Match if Subject contains this |
907
+ | `bodyContains` | string | No | Match if Body contains this |
908
+
909
+ Provide at least one of the three `*Contains` fields.
910
+
911
+ **⚠️ Safety:** edits `SyncedSmartMailboxes.plist` (backed up + atomic, existing smart mailboxes preserved). Quit Mail first for reliable results; the new smart mailbox appears the next time Mail launches.
912
+
913
+ ---
914
+
915
+ #### `delete-smart-mailbox`
916
+
917
+ Delete a smart mailbox by name.
918
+
919
+ | Parameter | Type | Required | Description |
920
+ |-----------|------|----------|-------------|
921
+ | `name` | string | Yes | Smart mailbox name |
922
+
923
+ **⚠️ Safety:** destructive — removes the smart mailbox from `SyncedSmartMailboxes.plist` (backed up + atomic; every other smart mailbox is preserved). Not undoable in-app. Confirm the exact name with `list-smart-mailboxes` first, and quit Mail first for reliable results.
924
+
925
+ ---
926
+
927
+ #### `create-newsletter-smart-mailboxes`
928
+
929
+ High-level tool: scan recent messages in your INBOXes, detect likely newsletters (volume + signals like List-Unsubscribe, noreply, repetitive subjects), and create smart mailboxes for them (names prefixed "NL: ...").
930
+
931
+ | Parameter | Type | Required | Description |
932
+ |-----------|------|----------|-------------|
933
+ | `dryRun` | boolean | No | Default true — only propose, do not create |
934
+ | `minCount` | number | No | Min messages from a sender (default 3) |
935
+ | `days` | number | No | Lookback window in days (default 90) |
936
+
937
+ Defaults to a **safe dry run** that only proposes. Pass `dryRun: false` to actually create the smart mailboxes for newsletters cluttering your Inbox.
938
+
939
+ **⚠️ Safety:** with `dryRun: false` this edits `SyncedSmartMailboxes.plist` (backed up + atomic, existing entries preserved) and can create many smart mailboxes at once — review a dry run first. Scans up to ~400 recent messages per inbox via AppleScript, which can be slow on large mailboxes.
940
+
941
+ ---
942
+
882
943
  ### Account Operations
883
944
 
884
945
  #### `list-accounts`
package/build/index.js CHANGED
@@ -75695,6 +75695,10 @@ import {
75695
75695
  existsSync as existsSync3,
75696
75696
  writeFileSync as writeFileSync3,
75697
75697
  readFileSync as readFileSync2,
75698
+ readdirSync as readdirSync2,
75699
+ unlinkSync,
75700
+ copyFileSync,
75701
+ renameSync,
75698
75702
  mkdtempSync as mkdtempSync2,
75699
75703
  rmSync as rmSync2,
75700
75704
  realpathSync,
@@ -75702,6 +75706,7 @@ import {
75702
75706
  } from "fs";
75703
75707
  import { isAbsolute, resolve, sep, join as join4 } from "path";
75704
75708
  import { homedir as homedir3 } from "os";
75709
+ import { randomUUID } from "crypto";
75705
75710
 
75706
75711
  // src/utils/applescript.ts
75707
75712
  import { execSync, spawnSync } from "child_process";
@@ -78678,6 +78683,386 @@ var AppleMailManager = class {
78678
78683
  return { success: true };
78679
78684
  }
78680
78685
  // ===========================================================================
78686
+ // Smart Mailbox (intelligente Postfächer) Operations
78687
+ // Uses plist manipulation because AppleScript terms for "smart mailbox"
78688
+ // / "intelligentes Postfach" do not compile reliably on German-localized
78689
+ // macOS (verified via osascript + JXA). Plist format is stable and
78690
+ // gives full control over criteria without UI/GUI scripting.
78691
+ // ===========================================================================
78692
+ findSyncedSmartPlist() {
78693
+ const base = join4(homedir3(), "Library", "Mail");
78694
+ try {
78695
+ const versions = readdirSync2(base).filter((d) => d.startsWith("V"));
78696
+ versions.sort().reverse();
78697
+ for (const v of versions) {
78698
+ const p = join4(base, v, "MailData", "SyncedSmartMailboxes.plist");
78699
+ if (existsSync3(p)) return p;
78700
+ }
78701
+ } catch {
78702
+ }
78703
+ return null;
78704
+ }
78705
+ /**
78706
+ * Read all smart-mailbox entries without ever mutating the plist.
78707
+ *
78708
+ * Fast path: `plutil -convert json`. That converter *rejects* any plist
78709
+ * containing <data>/<date> values ("Invalid object in plist for JSON
78710
+ * format") — and smart-mailbox date criteria can contain exactly those — so
78711
+ * on such libraries we fall back to structural probing with PlistBuddy. (The
78712
+ * previous implementation used the json path unconditionally and, on
78713
+ * failure, treated the whole file as empty, which then overwrote every
78714
+ * existing smart mailbox on the next save.)
78715
+ */
78716
+ readSmartMailboxEntries(plistPath) {
78717
+ if (!plistPath || !existsSync3(plistPath)) return [];
78718
+ const j = spawnSync2("plutil", ["-convert", "json", "-o", "-", plistPath], {
78719
+ encoding: "utf8"
78720
+ });
78721
+ if (j.status === 0 && j.stdout) {
78722
+ try {
78723
+ const data = JSON.parse(j.stdout);
78724
+ if (Array.isArray(data)) {
78725
+ return data.map((m) => ({
78726
+ name: m?.MailboxName ?? "",
78727
+ id: m?.MailboxID,
78728
+ criteriaSummary: this.summarizeCriteria(m?.MailboxCriteria)
78729
+ }));
78730
+ }
78731
+ } catch {
78732
+ }
78733
+ }
78734
+ return this.probeSmartMailboxEntries(plistPath);
78735
+ }
78736
+ /**
78737
+ * Structural enumeration for plists the json converter rejects (date/data
78738
+ * criteria). Probes array indices via PlistBuddy until one is out of range.
78739
+ */
78740
+ probeSmartMailboxEntries(plistPath) {
78741
+ const buddy = "/usr/libexec/PlistBuddy";
78742
+ const out = [];
78743
+ for (let i = 0; i < 1e3; i++) {
78744
+ const exists = spawnSync2(buddy, ["-c", `Print :${i}`, plistPath], { encoding: "utf8" });
78745
+ if (exists.status !== 0) break;
78746
+ const nameR = spawnSync2(buddy, ["-c", `Print :${i}:MailboxName`, plistPath], {
78747
+ encoding: "utf8"
78748
+ });
78749
+ const idR = spawnSync2(buddy, ["-c", `Print :${i}:MailboxID`, plistPath], {
78750
+ encoding: "utf8"
78751
+ });
78752
+ out.push({
78753
+ name: nameR.status === 0 ? (nameR.stdout || "").trim() : "",
78754
+ id: idR.status === 0 ? (idR.stdout || "").trim() || void 0 : void 0
78755
+ });
78756
+ }
78757
+ return out;
78758
+ }
78759
+ summarizeCriteria(crits) {
78760
+ if (!Array.isArray(crits)) return void 0;
78761
+ const summary = crits.map((c) => {
78762
+ const n = c?.Name || c?.Header || "";
78763
+ return c?.Expression ? `${n}=${String(c.Expression).slice(0, 25)}` : n;
78764
+ }).filter(Boolean).join("; ").slice(0, 100);
78765
+ return summary || void 0;
78766
+ }
78767
+ /**
78768
+ * Back up `plistPath` to `<plist>.bak` and return a sibling temp-file path
78769
+ * (same directory → same filesystem, so the later rename is atomic) seeded
78770
+ * with the current contents. Callers mutate the temp copy, then
78771
+ * commitSmartPlist() lints and atomically renames it into place — so the live
78772
+ * plist is only ever replaced wholesale by a validated file, never edited in
78773
+ * place and never left half-written.
78774
+ */
78775
+ prepareSmartPlistWrite(plistPath) {
78776
+ copyFileSync(plistPath, `${plistPath}.bak`);
78777
+ const temp = `${plistPath}.tmp-${randomUUID()}`;
78778
+ copyFileSync(plistPath, temp);
78779
+ return temp;
78780
+ }
78781
+ /** Lint the mutated temp file and atomically move it into place. */
78782
+ commitSmartPlist(temp, plistPath) {
78783
+ const lint = spawnSync2("plutil", ["-lint", temp], { encoding: "utf8" });
78784
+ if (lint.status !== 0) {
78785
+ try {
78786
+ unlinkSync(temp);
78787
+ } catch {
78788
+ }
78789
+ return false;
78790
+ }
78791
+ renameSync(temp, plistPath);
78792
+ return true;
78793
+ }
78794
+ buildSmartMailboxEntry(name, fromContains = "", subjectContains = "", bodyContains = "") {
78795
+ const newUuid = () => randomUUID().toUpperCase();
78796
+ const userExpr = fromContains || subjectContains || bodyContains || "";
78797
+ const userHeader = fromContains ? "From" : subjectContains ? "Subject" : "Body";
78798
+ const userCriterion = {
78799
+ AllCriteriaMustBeSatisfied: true,
78800
+ Criteria: [
78801
+ {
78802
+ CriterionUniqueId: newUuid(),
78803
+ Expression: userExpr,
78804
+ Header: userHeader
78805
+ }
78806
+ ],
78807
+ CriterionUniqueId: newUuid(),
78808
+ Header: "Compound",
78809
+ Name: "user criteria"
78810
+ };
78811
+ return {
78812
+ IMAPMailboxAttributes: 17,
78813
+ MailboxAllCriteriaMustBeSatisfied: true,
78814
+ MailboxChildren: [],
78815
+ MailboxCriteria: [
78816
+ { CriterionUniqueId: newUuid(), Header: "NotInTrashMailbox", Name: "omit trash" },
78817
+ {
78818
+ CriterionUniqueId: newUuid(),
78819
+ Header: "NotInASpecialMailbox",
78820
+ Name: "omit sent",
78821
+ SpecialMailboxType: 3
78822
+ },
78823
+ userCriterion,
78824
+ { CriterionUniqueId: newUuid(), Header: "NotInJunkMailbox", Name: "omit junk" }
78825
+ ],
78826
+ MailboxID: newUuid(),
78827
+ MailboxName: name,
78828
+ MailboxType: 7
78829
+ };
78830
+ }
78831
+ /**
78832
+ * List all smart mailboxes (intelligente Postfächer).
78833
+ * Reads from the synced plist (works on German + English systems).
78834
+ */
78835
+ listSmartMailboxes() {
78836
+ const plist = this.findSyncedSmartPlist();
78837
+ return this.readSmartMailboxEntries(plist).map((m) => ({
78838
+ name: m.name || "",
78839
+ id: m.id,
78840
+ criteriaSummary: m.criteriaSummary
78841
+ }));
78842
+ }
78843
+ /**
78844
+ * Create a new smart mailbox with a simple contains criterion.
78845
+ * Provide at least one of fromContains / subjectContains / bodyContains.
78846
+ *
78847
+ * The new entry is appended to SyncedSmartMailboxes.plist with a lossless
78848
+ * `plutil -insert -json` on a backed-up temp copy — existing smart mailboxes
78849
+ * (including any with date/data criteria) are preserved byte-for-byte, and
78850
+ * the live file is only ever replaced by a lint-validated copy. Does NOT quit
78851
+ * or restart Mail; the new mailbox appears the next time Mail is launched.
78852
+ */
78853
+ createSmartMailbox(name, fromContains = "", subjectContains = "", bodyContains = "") {
78854
+ if (!name || !fromContains && !subjectContains && !bodyContains) {
78855
+ return {
78856
+ created: false,
78857
+ alreadyExisted: false,
78858
+ error: "Provide a name and at least one of fromContains / subjectContains / bodyContains"
78859
+ };
78860
+ }
78861
+ const plist = this.findSyncedSmartPlist();
78862
+ if (!plist) {
78863
+ return {
78864
+ created: false,
78865
+ alreadyExisted: false,
78866
+ error: "No SyncedSmartMailboxes.plist found (launch Mail at least once)"
78867
+ };
78868
+ }
78869
+ return this.createSmartMailboxAtPath(plist, name, fromContains, subjectContains, bodyContains);
78870
+ }
78871
+ /** Path-injectable core of createSmartMailbox (unit-testable against a fixture plist). */
78872
+ createSmartMailboxAtPath(plistPath, name, fromContains = "", subjectContains = "", bodyContains = "") {
78873
+ const entries = this.readSmartMailboxEntries(plistPath);
78874
+ if (entries.some((e) => e.name === name)) {
78875
+ return { created: false, alreadyExisted: true };
78876
+ }
78877
+ const entry = this.buildSmartMailboxEntry(name, fromContains, subjectContains, bodyContains);
78878
+ const temp = this.prepareSmartPlistWrite(plistPath);
78879
+ const ins = spawnSync2(
78880
+ "plutil",
78881
+ ["-insert", String(entries.length), "-json", JSON.stringify(entry), temp],
78882
+ { encoding: "utf8" }
78883
+ );
78884
+ if (ins.status !== 0) {
78885
+ try {
78886
+ unlinkSync(temp);
78887
+ } catch {
78888
+ }
78889
+ return {
78890
+ created: false,
78891
+ alreadyExisted: false,
78892
+ error: (ins.stderr || "plutil insert failed").trim()
78893
+ };
78894
+ }
78895
+ if (!this.commitSmartPlist(temp, plistPath)) {
78896
+ return {
78897
+ created: false,
78898
+ alreadyExisted: false,
78899
+ error: "Edited plist failed validation; original left untouched"
78900
+ };
78901
+ }
78902
+ return { created: true, alreadyExisted: false };
78903
+ }
78904
+ /**
78905
+ * Delete a smart mailbox by name. Removes exactly the matching entry via
78906
+ * PlistBuddy on a backed-up temp copy; every other smart mailbox is
78907
+ * preserved. Does NOT quit or restart Mail.
78908
+ */
78909
+ deleteSmartMailbox(name) {
78910
+ const plist = this.findSyncedSmartPlist();
78911
+ if (!plist) {
78912
+ return { deleted: false, error: "No SyncedSmartMailboxes.plist found" };
78913
+ }
78914
+ return this.deleteSmartMailboxAtPath(plist, name);
78915
+ }
78916
+ /** Path-injectable core of deleteSmartMailbox (unit-testable against a fixture plist). */
78917
+ deleteSmartMailboxAtPath(plistPath, name) {
78918
+ const entries = this.readSmartMailboxEntries(plistPath);
78919
+ const idx = entries.findIndex((e) => e.name === name);
78920
+ if (idx < 0) {
78921
+ return { deleted: false, error: `Smart mailbox "${name}" not found` };
78922
+ }
78923
+ const temp = this.prepareSmartPlistWrite(plistPath);
78924
+ const del = spawnSync2("/usr/libexec/PlistBuddy", ["-c", `Delete :${idx}`, temp], {
78925
+ encoding: "utf8"
78926
+ });
78927
+ if (del.status !== 0) {
78928
+ try {
78929
+ unlinkSync(temp);
78930
+ } catch {
78931
+ }
78932
+ return { deleted: false, error: (del.stderr || "PlistBuddy delete failed").trim() };
78933
+ }
78934
+ if (!this.commitSmartPlist(temp, plistPath)) {
78935
+ return { deleted: false, error: "Edited plist failed validation; original left untouched" };
78936
+ }
78937
+ return { deleted: true };
78938
+ }
78939
+ // --- Newsletter smart mailbox discovery (high level helper) ---
78940
+ extractEmail(sender) {
78941
+ const m = /<([^>]+)>/.exec(sender || "");
78942
+ return m ? m[1].toLowerCase().trim() : (sender || "").toLowerCase().trim();
78943
+ }
78944
+ /**
78945
+ * Scan recent INBOX messages and return raw [sender, subject, source] rows.
78946
+ * This is the only AppleScript-touching part of newsletter discovery; the
78947
+ * grouping/scoring is factored into groupAndScoreNewsletters() so it can be
78948
+ * unit-tested without a running Mail.
78949
+ */
78950
+ scanInboxRows(days) {
78951
+ const script = `
78952
+ tell application "Mail"
78953
+ set outLines to ""
78954
+ set cutoff to (current date) - (${days} * days)
78955
+ repeat with acc in accounts
78956
+ repeat with mb in mailboxes of acc
78957
+ if name of mb is "INBOX" or name of mb is "Inbox" then
78958
+ set msgs to (messages of mb whose date received > cutoff)
78959
+ set cnt to 0
78960
+ repeat with m in msgs
78961
+ if cnt > 400 then exit repeat
78962
+ try
78963
+ set snd to (sender of m as text)
78964
+ set subj to (subject of m as text)
78965
+ set src to (source of m as text)
78966
+ set outLines to outLines & snd & "|" & subj & "|" & src & linefeed
78967
+ set cnt to cnt + 1
78968
+ end try
78969
+ end repeat
78970
+ end if
78971
+ end repeat
78972
+ end repeat
78973
+ return outLines
78974
+ end tell`;
78975
+ const res = executeAppleScript(script, { timeoutMs: 12e4 });
78976
+ if (!res.success || !res.output) return [];
78977
+ const rows = [];
78978
+ for (const line of res.output.split("\n")) {
78979
+ if (!line.includes("|")) continue;
78980
+ const [snd = "", subj = "", src = ""] = line.split("|", 3);
78981
+ rows.push({ sender: snd, subject: subj, source: src });
78982
+ }
78983
+ return rows;
78984
+ }
78985
+ /**
78986
+ * Pure grouping + scoring over scanned rows. Groups by sender email, keeps
78987
+ * senders at/above minCount, and scores by volume plus newsletter signals
78988
+ * (List-Unsubscribe, noreply/newsletter keywords, repetitive subjects).
78989
+ */
78990
+ groupAndScoreNewsletters(rows, minCount) {
78991
+ const groups = {};
78992
+ for (const { sender: snd, subject: subj, source: src } of rows) {
78993
+ const email2 = this.extractEmail(snd);
78994
+ if (!email2 || !email2.includes("@")) continue;
78995
+ if (!groups[email2]) {
78996
+ groups[email2] = { email: email2, sender: snd, count: 0, subjects: [], sample: "" };
78997
+ }
78998
+ const g = groups[email2];
78999
+ g.count++;
79000
+ if (g.subjects.length < 6) g.subjects.push(subj);
79001
+ if (!g.sample) g.sample = (src || "").slice(0, 3e3);
79002
+ }
79003
+ const out = [];
79004
+ for (const g of Object.values(groups)) {
79005
+ if (g.count < minCount) continue;
79006
+ let score = Math.min(g.count / 3, 8);
79007
+ const blob = g.email + " " + (g.sample || "").toLowerCase();
79008
+ const signals = [];
79009
+ if (/newsletter|digest|noreply|no-reply|list-unsubscribe/.test(blob)) {
79010
+ score += 4;
79011
+ signals.push("keyword_or_list");
79012
+ }
79013
+ if (g.sample && /list-unsubscribe/i.test(g.sample)) {
79014
+ score += 3;
79015
+ signals.push("list_unsubscribe");
79016
+ }
79017
+ const prefixes = g.subjects.slice(0, 5).map((s) => s.slice(0, 30));
79018
+ if (prefixes.length >= 2 && new Set(prefixes).size <= 2) {
79019
+ score += 2;
79020
+ signals.push("repetitive_subject");
79021
+ }
79022
+ const short = (g.sender.split("<")[0] || g.email).trim().slice(0, 30);
79023
+ out.push({
79024
+ email: g.email,
79025
+ sender: g.sender.slice(0, 80),
79026
+ count: g.count,
79027
+ score: Math.max(0.1, Math.round(score * 10) / 10),
79028
+ signals,
79029
+ suggestedName: `NL: ${short}`
79030
+ });
79031
+ }
79032
+ out.sort((a, b) => b.score - a.score);
79033
+ return out.slice(0, 50);
79034
+ }
79035
+ /**
79036
+ * Scan recent messages and return likely newsletter senders with scores.
79037
+ */
79038
+ findNewsletterCandidates(days = 90, minCount = 3) {
79039
+ return this.groupAndScoreNewsletters(this.scanInboxRows(days), minCount);
79040
+ }
79041
+ /**
79042
+ * High-level: discover likely newsletters from INBOX and (optionally) create smart mailboxes for them.
79043
+ */
79044
+ createNewsletterSmartMailboxes(dryRun = true, minCount = 3, days = 90) {
79045
+ const cands = this.findNewsletterCandidates(days, minCount);
79046
+ const results = [];
79047
+ for (const c of cands) {
79048
+ const nm = c.suggestedName;
79049
+ if (dryRun) {
79050
+ results.push({ name: nm, email: c.email, wouldCreate: true, score: c.score });
79051
+ continue;
79052
+ }
79053
+ const r = this.createSmartMailbox(nm, c.email);
79054
+ results.push({
79055
+ name: nm,
79056
+ email: c.email,
79057
+ success: r.created || r.alreadyExisted,
79058
+ alreadyExisted: r.alreadyExisted,
79059
+ error: r.error,
79060
+ score: c.score
79061
+ });
79062
+ }
79063
+ return { dryRun, createdOrProposed: results, count: results.length };
79064
+ }
79065
+ // ===========================================================================
78681
79066
  // Account Operations
78682
79067
  // ===========================================================================
78683
79068
  /**
@@ -79895,6 +80280,7 @@ async function dropPool(key) {
79895
80280
  if (e.idle) clearTimeout(e.idle);
79896
80281
  pools.delete(key);
79897
80282
  await e.client.logout().catch(() => void 0);
80283
+ e.client.close?.();
79898
80284
  }
79899
80285
  async function dropAllPools() {
79900
80286
  await Promise.all([...pools.keys()].map((k) => dropPool(k)));
@@ -79968,6 +80354,7 @@ async function useClient(deps, fn, retryOnDrop = false) {
79968
80354
  return await fn(client, cfg);
79969
80355
  } finally {
79970
80356
  await client.logout().catch(() => void 0);
80357
+ client.close?.();
79971
80358
  }
79972
80359
  }
79973
80360
  const key = poolKey(cfg);
@@ -82379,6 +82766,135 @@ server.registerTool(
82379
82766
  });
82380
82767
  }, "Error renaming mailbox")
82381
82768
  );
82769
+ server.registerTool(
82770
+ "list-smart-mailboxes",
82771
+ {
82772
+ description: "Use when: listing Apple Mail smart mailboxes (criteria-based virtual views), including on German-localized macOS where AppleScript's smart-mailbox terms do not compile.\nReturns: each smart mailbox's name and a short criteria summary.\nDo not use when: listing real folders/mailboxes (use list-mailboxes).",
82773
+ inputSchema: {},
82774
+ outputSchema: {
82775
+ count: external_exports.number().optional(),
82776
+ smartMailboxes: external_exports.array(
82777
+ external_exports.object({
82778
+ name: external_exports.string(),
82779
+ id: external_exports.string().optional(),
82780
+ criteriaSummary: external_exports.string().optional()
82781
+ })
82782
+ ).optional()
82783
+ }
82784
+ },
82785
+ withErrorHandling(() => {
82786
+ const list = mailManager.listSmartMailboxes();
82787
+ if (list.length === 0) {
82788
+ return successResponse("No smart mailboxes found", { count: 0, smartMailboxes: [] });
82789
+ }
82790
+ const lines = list.map((s) => ` - ${s.name}${s.criteriaSummary ? ` (${s.criteriaSummary})` : ""}`).join("\n");
82791
+ return successResponse(`Found ${list.length} smart mailbox(es):
82792
+ ${lines}`, {
82793
+ count: list.length,
82794
+ smartMailboxes: list.map((s) => ({
82795
+ name: s.name,
82796
+ id: s.id,
82797
+ criteriaSummary: s.criteriaSummary
82798
+ }))
82799
+ });
82800
+ }, "Error listing smart mailboxes")
82801
+ );
82802
+ server.registerTool(
82803
+ "create-smart-mailbox",
82804
+ {
82805
+ description: "Use when: creating an Apple Mail smart mailbox (a criteria-based virtual view) that matches a sender, subject, or body substring \u2014 works on German-localized macOS where AppleScript's smart-mailbox terms fail.\nReturns: confirmation of creation, or a note that a smart mailbox with that name already existed.\nDo not use when: creating a real folder (use create-mailbox).\nSafety: edits Apple Mail's SyncedSmartMailboxes.plist directly. It backs the file up (.bak) and writes atomically, and never rewrites your existing smart mailboxes. It does not quit Mail \u2014 quit Mail first for reliable results, since a running Mail may not show the new smart mailbox until relaunched and can overwrite plist edits it did not make.",
82806
+ inputSchema: {
82807
+ name: external_exports.string().min(1, "Smart mailbox name is required"),
82808
+ fromContains: external_exports.string().optional().describe("Match sender (From contains)"),
82809
+ subjectContains: external_exports.string().optional().describe("Match subject (contains)"),
82810
+ bodyContains: external_exports.string().optional().describe("Match body (contains)")
82811
+ },
82812
+ outputSchema: {
82813
+ ok: external_exports.boolean().optional(),
82814
+ name: external_exports.string().optional(),
82815
+ alreadyExisted: external_exports.boolean().optional()
82816
+ }
82817
+ },
82818
+ withErrorHandling(({ name, fromContains, subjectContains, bodyContains }) => {
82819
+ if (!fromContains && !subjectContains && !bodyContains) {
82820
+ return errorResponse("Provide at least one of fromContains / subjectContains / bodyContains");
82821
+ }
82822
+ const r = mailManager.createSmartMailbox(
82823
+ name,
82824
+ fromContains || "",
82825
+ subjectContains || "",
82826
+ bodyContains || ""
82827
+ );
82828
+ if (r.alreadyExisted) {
82829
+ return successResponse(`Smart mailbox "${name}" already exists`, {
82830
+ ok: true,
82831
+ name,
82832
+ alreadyExisted: true
82833
+ });
82834
+ }
82835
+ if (!r.created) {
82836
+ return errorResponse(r.error || `Failed to create smart mailbox "${name}"`);
82837
+ }
82838
+ return successResponse(`Smart mailbox "${name}" created. Quit and reopen Mail to see it.`, {
82839
+ ok: true,
82840
+ name,
82841
+ alreadyExisted: false
82842
+ });
82843
+ }, "Error creating smart mailbox")
82844
+ );
82845
+ server.registerTool(
82846
+ "delete-smart-mailbox",
82847
+ {
82848
+ description: "Use when: deleting an Apple Mail smart mailbox (virtual view) by name.\nReturns: confirmation of deletion.\nDo not use when: deleting a real folder (use delete-mailbox) or messages (use delete-message / batch-delete-messages).\nSafety: destructive \u2014 removes the smart mailbox from Apple Mail's SyncedSmartMailboxes.plist. It backs the file up (.bak) and writes atomically, preserving every other smart mailbox, but the removal is not undoable in-app. Confirm the exact name with list-smart-mailboxes first, and quit Mail first for reliable results.",
82849
+ inputSchema: {
82850
+ name: external_exports.string().min(1, "Smart mailbox name is required")
82851
+ },
82852
+ outputSchema: {
82853
+ ok: external_exports.boolean().optional(),
82854
+ name: external_exports.string().optional()
82855
+ }
82856
+ },
82857
+ withErrorHandling(({ name }) => {
82858
+ const r = mailManager.deleteSmartMailbox(name);
82859
+ if (!r.deleted) {
82860
+ return errorResponse(r.error || `Failed to delete smart mailbox "${name}"`);
82861
+ }
82862
+ return successResponse(`Smart mailbox "${name}" deleted. Quit and reopen Mail to refresh.`, {
82863
+ ok: true,
82864
+ name
82865
+ });
82866
+ }, "Error deleting smart mailbox")
82867
+ );
82868
+ server.registerTool(
82869
+ "create-newsletter-smart-mailboxes",
82870
+ {
82871
+ description: `Use when: auto-discovering newsletter/bulk senders in your INBOX(es) and (optionally) creating a dedicated smart mailbox per sender (named "NL: <sender>"). Defaults to a safe dry run that only proposes.
82872
+ Returns: the proposed or created smart mailboxes with their match scores.
82873
+ Do not use when: you already know the exact sender (use create-smart-mailbox) or want real folders (use create-mailbox).
82874
+ Safety: with dryRun=false it edits Apple Mail's SyncedSmartMailboxes.plist (backed up, atomic, existing entries preserved) and can create many smart mailboxes at once \u2014 review a dryRun first. It scans up to ~400 recent messages per inbox via AppleScript, which can be slow on large mailboxes.`,
82875
+ inputSchema: {
82876
+ dryRun: external_exports.boolean().default(true).describe("If true (default), only propose; if false, actually create"),
82877
+ minCount: external_exports.number().int().min(1).default(3).describe("Minimum messages from sender in the period"),
82878
+ days: external_exports.number().int().min(1).default(90).describe("Look back this many days in INBOXes")
82879
+ },
82880
+ outputSchema: {
82881
+ dryRun: external_exports.boolean().optional(),
82882
+ count: external_exports.number().optional()
82883
+ }
82884
+ },
82885
+ withErrorHandling(({ dryRun, minCount, days }) => {
82886
+ const result = mailManager.createNewsletterSmartMailboxes(!!dryRun, minCount, days);
82887
+ const lines = result.createdOrProposed.map(
82888
+ (c) => ` - ${c.name || c.suggestedName || c.email} (score ${c.score ?? "?"}${c.wouldCreate ? ", dry-run" : c.alreadyExisted ? ", already existed" : c.error ? `, error: ${c.error}` : ""})`
82889
+ ).join("\n");
82890
+ const prefix = result.dryRun ? "DRY RUN - would create" : "Created";
82891
+ return successResponse(
82892
+ `${prefix} ${result.count} newsletter smart mailbox(es):
82893
+ ${lines || " (none met the threshold)"}`,
82894
+ { dryRun: result.dryRun, count: result.count }
82895
+ );
82896
+ }, "Error creating newsletter smart mailboxes")
82897
+ );
82382
82898
  server.registerTool(
82383
82899
  "list-accounts",
82384
82900
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.8.15",
3
+ "version": "2.9.0",
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",