apple-mail-mcp 2.8.16 → 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.
- package/README.md +61 -0
- package/build/index.js +514 -0
- 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
|
/**
|
|
@@ -82381,6 +82766,135 @@ server.registerTool(
|
|
|
82381
82766
|
});
|
|
82382
82767
|
}, "Error renaming mailbox")
|
|
82383
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
|
+
);
|
|
82384
82898
|
server.registerTool(
|
|
82385
82899
|
"list-accounts",
|
|
82386
82900
|
{
|
package/package.json
CHANGED