apple-mail-mcp 2.10.30 → 2.10.31
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 +7 -0
- package/build/index.js +87 -60
- package/docs/THREAT-MODEL.md +171 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1549,6 +1549,13 @@ AI: [calls move-message for each, with mailbox="Archive"]
|
|
|
1549
1549
|
|
|
1550
1550
|
---
|
|
1551
1551
|
|
|
1552
|
+
## Documentation
|
|
1553
|
+
|
|
1554
|
+
- [Threat model](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/THREAT-MODEL.md)
|
|
1555
|
+
- [IMAP / SMTP setup guide](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md)
|
|
1556
|
+
- [Node runtime and TCC permissions](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md)
|
|
1557
|
+
- [Stability and performance audit](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/STABILITY-PERF-AUDIT-2026-06-17.md)
|
|
1558
|
+
|
|
1552
1559
|
## Installation Options
|
|
1553
1560
|
|
|
1554
1561
|
### npm (Recommended)
|
package/build/index.js
CHANGED
|
@@ -80959,6 +80959,12 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
80959
80959
|
if (resolved.kind === "unresolvable") {
|
|
80960
80960
|
return operands.map((id) => ({ id, success: false, error: resolved.error }));
|
|
80961
80961
|
}
|
|
80962
|
+
if (resolved.kind === "scoped") {
|
|
80963
|
+
const disabled = this.disabledAccountGuard(resolved.account);
|
|
80964
|
+
if (disabled) {
|
|
80965
|
+
return operands.map((id) => ({ id, success: false, error: disabled }));
|
|
80966
|
+
}
|
|
80967
|
+
}
|
|
80962
80968
|
const callerScope = resolved.kind === "scoped" ? resolved : void 0;
|
|
80963
80969
|
const groups = /* @__PURE__ */ new Map();
|
|
80964
80970
|
const unlocated = [];
|
|
@@ -84006,6 +84012,72 @@ ${warnings.join("\n")}` : "";
|
|
|
84006
84012
|
return successResponse(`${messages.partial(success, fail)}${suffix}${warn}`, structured);
|
|
84007
84013
|
}
|
|
84008
84014
|
|
|
84015
|
+
// src/tools/batchMutations.ts
|
|
84016
|
+
function toManagerScope(args) {
|
|
84017
|
+
return { account: args.sourceAccount, mailbox: args.sourceMailbox };
|
|
84018
|
+
}
|
|
84019
|
+
async function runBatchDelete(deps, args) {
|
|
84020
|
+
const { ids, sourceMailbox, sourceAccount } = args;
|
|
84021
|
+
let forensics = { warnings: [] };
|
|
84022
|
+
const counts = await hybridBatchCounts(
|
|
84023
|
+
ids,
|
|
84024
|
+
(n) => {
|
|
84025
|
+
const res = deps.batchDeleteMessages(n, toManagerScope({ sourceAccount, sourceMailbox }));
|
|
84026
|
+
forensics = deps.collectForensics("batch-delete-messages", {
|
|
84027
|
+
ids,
|
|
84028
|
+
sourceMailbox,
|
|
84029
|
+
sourceAccount
|
|
84030
|
+
});
|
|
84031
|
+
return res;
|
|
84032
|
+
},
|
|
84033
|
+
(im) => deps.imapBatchDelete(im)
|
|
84034
|
+
);
|
|
84035
|
+
return batchResponse(
|
|
84036
|
+
counts,
|
|
84037
|
+
{
|
|
84038
|
+
allSucceeded: (n) => `Successfully deleted ${n} message(s)`,
|
|
84039
|
+
allFailed: (n) => `Failed to delete all ${n} message(s)`,
|
|
84040
|
+
partial: (ok, failed) => `Deleted ${ok} message(s), ${failed} failed`
|
|
84041
|
+
},
|
|
84042
|
+
forensics.countDelta ? { countDelta: forensics.countDelta } : {},
|
|
84043
|
+
forensics.warnings
|
|
84044
|
+
);
|
|
84045
|
+
}
|
|
84046
|
+
async function runBatchMove(deps, args) {
|
|
84047
|
+
const { ids, mailbox, account, sourceMailbox, sourceAccount } = args;
|
|
84048
|
+
let forensics = { warnings: [] };
|
|
84049
|
+
const counts = await hybridBatchCounts(
|
|
84050
|
+
ids,
|
|
84051
|
+
(n) => {
|
|
84052
|
+
const res = deps.batchMoveMessages(
|
|
84053
|
+
n,
|
|
84054
|
+
mailbox,
|
|
84055
|
+
account,
|
|
84056
|
+
toManagerScope({ sourceAccount, sourceMailbox })
|
|
84057
|
+
);
|
|
84058
|
+
forensics = deps.collectForensics("batch-move-messages", {
|
|
84059
|
+
ids,
|
|
84060
|
+
mailbox,
|
|
84061
|
+
account,
|
|
84062
|
+
sourceMailbox,
|
|
84063
|
+
sourceAccount
|
|
84064
|
+
});
|
|
84065
|
+
return res;
|
|
84066
|
+
},
|
|
84067
|
+
(im) => deps.imapBatchMove(im, mailbox, { account })
|
|
84068
|
+
);
|
|
84069
|
+
return batchResponse(
|
|
84070
|
+
counts,
|
|
84071
|
+
{
|
|
84072
|
+
allSucceeded: (n) => `Successfully moved ${n} message(s) to "${mailbox}"`,
|
|
84073
|
+
allFailed: (n) => `Failed to move all ${n} message(s)`,
|
|
84074
|
+
partial: (ok, failed) => `Moved ${ok} message(s) to "${mailbox}", ${failed} failed`
|
|
84075
|
+
},
|
|
84076
|
+
{ mailbox, ...forensics.countDelta ? { countDelta: forensics.countDelta } : {} },
|
|
84077
|
+
forensics.warnings
|
|
84078
|
+
);
|
|
84079
|
+
}
|
|
84080
|
+
|
|
84009
84081
|
// src/services/imapMultiAccount.ts
|
|
84010
84082
|
function normalizeMessageId2(row) {
|
|
84011
84083
|
const raw = typeof row.messageId === "string" ? row.messageId.trim() : "";
|
|
@@ -85578,6 +85650,13 @@ ${warnings.join("\n")}` : ""}`,
|
|
|
85578
85650
|
"Error moving message"
|
|
85579
85651
|
)
|
|
85580
85652
|
);
|
|
85653
|
+
var batchMutationDeps = {
|
|
85654
|
+
batchDeleteMessages: (ids, scope) => mailManager.batchDeleteMessages(ids, scope),
|
|
85655
|
+
batchMoveMessages: (ids, mailbox, account, scope) => mailManager.batchMoveMessages(ids, mailbox, account, scope),
|
|
85656
|
+
imapBatchDelete: (ids) => imapBatchDelete(ids),
|
|
85657
|
+
imapBatchMove: (ids, mailbox, opts) => imapBatchMove(ids, mailbox, opts),
|
|
85658
|
+
collectForensics: (tool, args) => collectForensics(tool, args)
|
|
85659
|
+
};
|
|
85581
85660
|
registerTool(
|
|
85582
85661
|
"batch-delete-messages",
|
|
85583
85662
|
{
|
|
@@ -85589,35 +85668,10 @@ registerTool(
|
|
|
85589
85668
|
},
|
|
85590
85669
|
outputSchema: { ...BATCH_COUNT_OUTPUT_SCHEMA, countDelta: COUNT_DELTA_OUTPUT_SCHEMA }
|
|
85591
85670
|
},
|
|
85592
|
-
withErrorHandling(
|
|
85593
|
-
|
|
85594
|
-
|
|
85595
|
-
|
|
85596
|
-
(n) => {
|
|
85597
|
-
const res = mailManager.batchDeleteMessages(n, {
|
|
85598
|
-
account: sourceAccount,
|
|
85599
|
-
mailbox: sourceMailbox
|
|
85600
|
-
});
|
|
85601
|
-
forensics = collectForensics("batch-delete-messages", {
|
|
85602
|
-
ids,
|
|
85603
|
-
sourceMailbox,
|
|
85604
|
-
sourceAccount
|
|
85605
|
-
});
|
|
85606
|
-
return res;
|
|
85607
|
-
},
|
|
85608
|
-
(im) => imapBatchDelete(im)
|
|
85609
|
-
);
|
|
85610
|
-
return batchResponse(
|
|
85611
|
-
counts,
|
|
85612
|
-
{
|
|
85613
|
-
allSucceeded: (n) => `Successfully deleted ${n} message(s)`,
|
|
85614
|
-
allFailed: (n) => `Failed to delete all ${n} message(s)`,
|
|
85615
|
-
partial: (ok, failed) => `Deleted ${ok} message(s), ${failed} failed`
|
|
85616
|
-
},
|
|
85617
|
-
forensics.countDelta ? { countDelta: forensics.countDelta } : {},
|
|
85618
|
-
forensics.warnings
|
|
85619
|
-
);
|
|
85620
|
-
}, "Error batch deleting messages")
|
|
85671
|
+
withErrorHandling(
|
|
85672
|
+
async ({ ids, sourceMailbox, sourceAccount }) => runBatchDelete(batchMutationDeps, { ids, sourceMailbox, sourceAccount }),
|
|
85673
|
+
"Error batch deleting messages"
|
|
85674
|
+
)
|
|
85621
85675
|
);
|
|
85622
85676
|
registerTool(
|
|
85623
85677
|
"batch-move-messages",
|
|
@@ -85632,37 +85686,10 @@ registerTool(
|
|
|
85632
85686
|
},
|
|
85633
85687
|
outputSchema: { ...BATCH_COUNT_OUTPUT_SCHEMA, countDelta: COUNT_DELTA_OUTPUT_SCHEMA }
|
|
85634
85688
|
},
|
|
85635
|
-
withErrorHandling(
|
|
85636
|
-
|
|
85637
|
-
|
|
85638
|
-
|
|
85639
|
-
(n) => {
|
|
85640
|
-
const res = mailManager.batchMoveMessages(n, mailbox, account, {
|
|
85641
|
-
account: sourceAccount,
|
|
85642
|
-
mailbox: sourceMailbox
|
|
85643
|
-
});
|
|
85644
|
-
forensics = collectForensics("batch-move-messages", {
|
|
85645
|
-
ids,
|
|
85646
|
-
mailbox,
|
|
85647
|
-
account,
|
|
85648
|
-
sourceMailbox,
|
|
85649
|
-
sourceAccount
|
|
85650
|
-
});
|
|
85651
|
-
return res;
|
|
85652
|
-
},
|
|
85653
|
-
(im) => imapBatchMove(im, mailbox, { account })
|
|
85654
|
-
);
|
|
85655
|
-
return batchResponse(
|
|
85656
|
-
counts,
|
|
85657
|
-
{
|
|
85658
|
-
allSucceeded: (n) => `Successfully moved ${n} message(s) to "${mailbox}"`,
|
|
85659
|
-
allFailed: (n) => `Failed to move all ${n} message(s)`,
|
|
85660
|
-
partial: (ok, failed) => `Moved ${ok} message(s) to "${mailbox}", ${failed} failed`
|
|
85661
|
-
},
|
|
85662
|
-
{ mailbox, ...forensics.countDelta ? { countDelta: forensics.countDelta } : {} },
|
|
85663
|
-
forensics.warnings
|
|
85664
|
-
);
|
|
85665
|
-
}, "Error batch moving messages")
|
|
85689
|
+
withErrorHandling(
|
|
85690
|
+
async ({ ids, mailbox, account, sourceMailbox, sourceAccount }) => runBatchMove(batchMutationDeps, { ids, mailbox, account, sourceMailbox, sourceAccount }),
|
|
85691
|
+
"Error batch moving messages"
|
|
85692
|
+
)
|
|
85666
85693
|
);
|
|
85667
85694
|
registerTool(
|
|
85668
85695
|
"batch-mark-as-read",
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Apple Mail MCP Threat Model
|
|
2
|
+
|
|
3
|
+
## Executive summary
|
|
4
|
+
|
|
5
|
+
This repository is a macOS TypeScript MCP server with authority to read and mutate Apple Mail, send through Mail.app or optional SMTP, access optional IMAP accounts, read local attachment paths, write inbound attachments, and create persistent Mail rules. The highest-risk abuse paths are confused-deputy flows: a hostile email or misled model can turn a broad file path into outbound exfiltration, a numeric Mail id into a wrong-message mutation, or one enabled rule into durable deletion or movement. Existing escaping, path-prefix, mailbox-scoping, forensic, schema, and attachment-size controls are meaningful, but several privileged boundaries still rely on prose, mutable defaults, or broad filesystem and network authority.
|
|
6
|
+
|
|
7
|
+
## Scope and assumptions
|
|
8
|
+
|
|
9
|
+
- In scope: runtime behavior under `src/`, the committed `build/` artifact, plugin launch configuration under `codex/`, package and lockfile state, security policy, and Dependabot workflows.
|
|
10
|
+
- Out of scope: Apple Mail, Contacts, macOS TCC, the user's Keychain, IMAP/SMTP servers, the MCP host's implementation, and any remote bridge that may expose this stdio process. Those systems are modeled as external boundaries, not as repository code.
|
|
11
|
+
- Intended use: an MCP server used to control personal or organizational Apple Mail from an AI host.
|
|
12
|
+
- Native deployment: the repository starts a stdio MCP transport and supplies no authentication, tenant isolation, or authorization layer. Any host or wrapper that exposes it to remote or multi-user clients must provide authentication and authorization before forwarding calls.
|
|
13
|
+
- Optional services: SMTP and IMAP are inert until configured (`~/Library/Application Support/apple-mail-mcp/config.json` plus Keychain credentials). Every tool is registered unconditionally at startup, including the Contacts, smart-mailbox and filesystem-attachment tools — those are gated by macOS TCC and the filesystem boundary at call time, not by registration. The default AppleScript mail path is local to the Mac, but it still has the Mail.app authority granted by macOS TCC.
|
|
14
|
+
- Attachment writes: `save-attachment` is constrained to the home directory, `/tmp`, `/private/tmp`, and `/Volumes`; canonicalization, path-segment checks, traversal rejection, symlink checks, and exclusive creation protect the destination boundary.
|
|
15
|
+
- Data sensitivity: mailbox contents, attachment bytes, contact data, message metadata, credentials obtained through the macOS Keychain, SMTP/IMAP traffic, and local files reachable by attachment paths are sensitive.
|
|
16
|
+
- Attacker assumption: an attacker can influence email content, attachment names and bytes, prompt context, or a connected model's tool arguments, but cannot directly edit the local repository or invoke macOS APIs outside the MCP unless the host grants that path.
|
|
17
|
+
|
|
18
|
+
The ranking below treats hostile mail, prompt injection, and unsafe tool arguments as in scope. It treats remote exposure as conditional on a wrapper: the repository does not claim to secure a wrapper that it does not implement.
|
|
19
|
+
|
|
20
|
+
## System model
|
|
21
|
+
|
|
22
|
+
### Primary components
|
|
23
|
+
|
|
24
|
+
- `src/index.ts` is the MCP entrypoint. It registers tool schemas and handlers, routes numeric ids to AppleScript and `imap:` ids to IMAP, and starts `StdioServerTransport`.
|
|
25
|
+
- `src/services/appleMailManager.ts` is the AppleScript authority for account discovery, message reads and mutations, attachments, rules, contacts, and smart-mailbox operations. It executes `osascript` through `src/utils/applescript.ts`.
|
|
26
|
+
- `src/services/imapClient.ts` is the optional IMAP authority. It resolves credentials from environment or Keychain, maintains pooled connections, reads messages and attachments, and performs server-side mutations.
|
|
27
|
+
- `src/services/smtpMailer.ts` is the optional outbound network authority. It resolves SMTP credentials and sends MIME through Nodemailer.
|
|
28
|
+
- `src/utils/attachmentMaterialize.ts` and the attachment builders in `appleMailManager.ts` and `smtpMailer.ts` turn tool arguments into local file reads or temporary files.
|
|
29
|
+
- `codex/.mcp.json` and the plugin manifests define how a host starts the server. The Codex manifest pins an exact `apple-mail-mcp` version (#166) and `scripts/sync-plugin-version.mjs` rewrites that pin on every bump, but npm resolution still carries no integrity/hash binding.
|
|
30
|
+
- `.github/workflows/dependabot-automerge.yml` and `.github/workflows/dependabot-rebuild.yml` govern automated dependency changes and committed bundle regeneration. They are CI/build surfaces, not runtime controls.
|
|
31
|
+
|
|
32
|
+
### Data flows and trust boundaries
|
|
33
|
+
|
|
34
|
+
- MCP host -> `src/index.ts`: tool arguments, ids, mailbox names, recipient addresses, rule definitions, attachment paths, and base64 bytes cross a local stdio boundary. Zod schemas validate many shapes and sizes; there is no authentication or per-caller authorization in the repository.
|
|
35
|
+
- `src/index.ts` -> `appleMailManager.ts`: validated arguments cross an in-process boundary into AppleScript generation. Numeric values are coerced and many strings are escaped; authorization is mostly tool semantics and descriptions rather than a capability gate.
|
|
36
|
+
- `appleMailManager.ts` -> macOS Mail.app: Apple Events and `osascript` cross a privileged automation boundary. macOS TCC controls whether the process may automate Mail, but the repository does not distinguish callers or restrict which authenticated user may invoke a tool.
|
|
37
|
+
- MCP host -> local filesystem attachment readers: attachment paths are canonicalized and must be regular files inside the default user-content/temporary roots or an explicit configured root; symlink escapes are rejected before Mail.app or Nodemailer reads them.
|
|
38
|
+
- Mail.app or IMAP -> local filesystem attachment writers: email-controlled filenames and bytes cross into `saveAttachment` or the IMAP save handler. Save roots and traversal/symlink checks exist, and existing destinations are rejected by exclusive creation.
|
|
39
|
+
- `src/index.ts` -> `imapClient.ts` -> IMAP server: account labels, mailbox paths, UIDs, searches, attachment sections, and mutations cross an authenticated TLS boundary. IMAP ids encode account, path, and UID; attachment buffering remains a separate availability concern.
|
|
40
|
+
- `src/index.ts` -> `smtpMailer.ts` -> SMTP server: recipient data, message content, local attachment paths, and Keychain-backed credentials cross an outbound network boundary. Non-implicit-TLS SMTP requires STARTTLS.
|
|
41
|
+
- Developer or Dependabot PR -> GitHub Actions -> committed `build/`: dependency-selected code is installed and built in CI. The rebuild workflow recognizes that toolchain dependencies can alter shipped bytes, while the automerge workflow still treats every direct development dependency as safe to merge automatically.
|
|
42
|
+
- Plugin install -> npm registry -> launched server: `codex/.mcp.json` invokes `npx -y apple-mail-mcp@<exact-version>` (pinned by #166, kept in sync by CI). The version is no longer a separate trust claim, but npm resolution still has no integrity/hash binding, so a registry compromise could serve different bytes for that same version.
|
|
43
|
+
|
|
44
|
+
#### Diagram
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
flowchart TD
|
|
48
|
+
host["MCP host and model"] --> server["MCP stdio server"]
|
|
49
|
+
server --> mail["Apple Mail and macOS automation"]
|
|
50
|
+
server --> files["Local filesystem"]
|
|
51
|
+
server --> imap["IMAP provider"]
|
|
52
|
+
server --> smtp["SMTP provider"]
|
|
53
|
+
host --> plugin["Plugin and package resolution"]
|
|
54
|
+
plugin --> server
|
|
55
|
+
developer["Developer or Dependabot"] --> ci["GitHub Actions and build"]
|
|
56
|
+
ci --> artifact["Committed runtime artifact"]
|
|
57
|
+
artifact --> plugin
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Assets and security objectives
|
|
61
|
+
|
|
62
|
+
| Asset | Why it matters | Security objective (C/I/A) |
|
|
63
|
+
| -------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------- |
|
|
64
|
+
| Mailbox contents and attachment bytes | Personal, business, and potentially regulated communications | C/I |
|
|
65
|
+
| Send authority and recipient lists | Misuse can create irreversible external communications or data leaks | I |
|
|
66
|
+
| SMTP and IMAP credentials | Keychain-backed credentials grant network mailbox access | C/I |
|
|
67
|
+
| Local files exposed as attachments | Home-directory files may contain tokens, keys, browser data, and private records | C |
|
|
68
|
+
| Inbound attachment destination files | Overwrite can corrupt configuration, shell startup, or user data | I/A |
|
|
69
|
+
| Persistent Mail rules | A rule can continue deleting or moving mail after the initiating conversation ends | I/A |
|
|
70
|
+
| Mailbox placement and message identity | A wrong numeric-id target can silently mutate a different copy | I |
|
|
71
|
+
| Committed `build/` and plugin launch configuration | These determine the bytes and authority users execute | I |
|
|
72
|
+
| MCP server availability and memory | Large attachment downloads or queued operations can starve the local agent | A |
|
|
73
|
+
| Audit and error records | Forensics must remain trustworthy during destructive operations | I |
|
|
74
|
+
|
|
75
|
+
## Attacker model
|
|
76
|
+
|
|
77
|
+
### Capabilities
|
|
78
|
+
|
|
79
|
+
- Supply or influence email subjects, bodies, attachment filenames, attachment bytes, and message metadata through a provider or a sender.
|
|
80
|
+
- Influence an AI model or MCP client through prompt injection, misleading mailbox content, or ambiguous natural-language requests so it emits valid but unsafe tool arguments.
|
|
81
|
+
- Reach the MCP through the configured host boundary. Under the selected broader-exposure assumption, this may include a remote or multi-user wrapper unless that wrapper adds authentication and authorization.
|
|
82
|
+
- Trigger valid paths, mailbox names, numeric ids, `imap:` ids, rule definitions, and network configuration values that the repository accepts.
|
|
83
|
+
- Cause large or repeated reads and attachment fetches within the limits the host imposes.
|
|
84
|
+
|
|
85
|
+
### Non-capabilities
|
|
86
|
+
|
|
87
|
+
- Direct repository write access, GitHub maintainer privileges, or the ability to change a committed workflow are not assumed for a runtime attacker.
|
|
88
|
+
- Direct Keychain extraction, Mail.app automation, or arbitrary local file access outside the server's granted process authority is not assumed; these are consequences to protect, not attacker prerequisites.
|
|
89
|
+
- A remote client is not considered authenticated merely because it can reach a wrapper. If the wrapper has no identity and authorization layer, that is an exposure assumption and a separate deployment defect.
|
|
90
|
+
|
|
91
|
+
## Entry points and attack surfaces
|
|
92
|
+
|
|
93
|
+
| Surface | How reached | Trust boundary | Notes | Evidence (repo path / symbol) |
|
|
94
|
+
| ------------------------- | -------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
95
|
+
| Tool arguments | MCP `tools/call` over stdio or a host wrapper | Host to runtime | Schemas validate shape but not caller authority | `src/index.ts` `registerTool` |
|
|
96
|
+
| Numeric message mutations | `delete-message`, `move-message`, six batch tools | Runtime to Mail.app | Numeric ids are mailbox-local; some paths still walk first match | `src/services/appleMailManager.ts` `replyToMessage`, `forwardMessage`, `findMessageScript` |
|
|
97
|
+
| Attachment path input | `send-email`, `create-draft`, CLI attachment flags | Runtime to local files and SMTP/Mail.app | Absolute and existing is not a confidentiality boundary | `src/index.ts` `ATTACHMENTS_SCHEMA`, `src/services/smtpMailer.ts` `buildAttachments` |
|
|
98
|
+
| Attachment save | `save-attachment` | Mail/IMAP to local filesystem | Traversal and final symlink checks exist; regular overwrite is not rejected | `resolveAttachmentSaveTarget`, `saveAttachment` |
|
|
99
|
+
| IMAP attachment fetch | `fetch-attachment`, IMAP save path | IMAP provider to process memory | buffering is capped by `MAX_IMAP_ATTACHMENT_BYTES`; oversize parts are refused mid-stream | `src/services/imapClient.ts` `streamToBuffer`, `imapFetchAttachment` |
|
|
100
|
+
| Persistent rules | `create-rule` | MCP to durable Mail.app automation | Default enabled state can make delete/move persistent | `src/index.ts` `create-rule`, `AppleMailManager.createRule` |
|
|
101
|
+
| Network credentials | SMTP/IMAP configuration and Keychain reads | Runtime to providers | TLS mode is not uniformly fail-closed | `src/services/smtpMailer.ts`, `src/services/imapClient.ts` |
|
|
102
|
+
| Runtime supply chain | Plugin install and MCP start | Registry/plugin to executable | `npx` resolves a pinned version with no integrity binding | `codex/.mcp.json` |
|
|
103
|
+
| CI artifact promotion | Dependabot PRs and workflow pushes | Developer intent to shipped build | Dev toolchain changes can alter `build/` | `.github/workflows/dependabot-automerge.yml`, `dependabot-rebuild.yml` |
|
|
104
|
+
|
|
105
|
+
## Top abuse paths
|
|
106
|
+
|
|
107
|
+
1. **Exfiltrate a local secret:** a malicious message or prompt convinces the model to attach an absolute path under the home directory -> the outbound builder reads it -> SMTP or Mail.app sends it to an attacker-controlled recipient.
|
|
108
|
+
2. **Corrupt a local file:** an attacker controls an inbound attachment filename and convinces the model to save it to a permitted directory -> the regular destination exists -> `writeFileSync` or Mail.app save replaces it.
|
|
109
|
+
3. **Mutate the wrong message:** a numeric id is duplicated across mailboxes or accounts -> reply, forward, or an implicitly account-resolved batch chooses the first/default match -> the server reports success for a different message.
|
|
110
|
+
4. **Install durable deletion:** a prompt injection supplies a valid `create-rule` request with `delete` or `moveTo` -> the rule is created enabled -> future mail is changed after the initiating conversation is over.
|
|
111
|
+
5. **Turn delete into purge:** an IMAP `delete-message` request names a message already in Trash -> the implementation calls `messageDelete` -> the message is permanently expunged despite the recoverable-delete contract.
|
|
112
|
+
6. **Exhaust the MCP process:** a provider advertises or streams a very large attachment -> `streamToBuffer` refuses the part once the running total exceeds `MAX_IMAP_ATTACHMENT_BYTES`, so the concatenated buffer and base64 representation are never allocated. Closed by #162.
|
|
113
|
+
7. **Expose credentials or mail:** a non-implicit-TLS SMTP or IMAP endpoint does not offer STARTTLS -> the client proceeds or relies on opportunistic behavior -> credentials or content can cross the network without the intended encryption guarantee.
|
|
114
|
+
8. **Run unreviewed code:** a plugin install starts `npx -y apple-mail-mcp` -> registry resolution selects a newer or compromised package than the manifest version -> the launched process inherits the host's Mail, filesystem, and network authority.
|
|
115
|
+
|
|
116
|
+
## Threat model table
|
|
117
|
+
|
|
118
|
+
| Threat ID | Threat source | Prerequisites | Threat action | Impact | Impacted assets | Existing controls (evidence) | Gaps | Recommended mitigations | Detection ideas | Likelihood | Impact severity | Priority |
|
|
119
|
+
| --------- | -------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------- | --------------- | ----------- |
|
|
120
|
+
| TM-001 | Malicious email or misled model | Outbound attachments enabled; model can choose a path and recipient | Read an arbitrary existing local path and send it as an attachment | Direct local-file exfiltration | Local files, mailbox data, send authority | Absolute/existing checks in `src/services/smtpMailer.ts`; inline base64 limit in `src/utils/attachmentLimits.ts` | No narrow read-root policy or enforced approval gate | Allow only configured dedicated read roots; resolve real paths and reject symlink escapes; require an explicit send capability and recipient confirmation outside prose | Audit attachment canonical path, recipient, transport, and rejection reason; alert on sensitive roots | high under broader exposure | high | critical |
|
|
121
|
+
| TM-002 | Malicious sender or misled model | Inbound attachment save enabled; destination already exists | Replace a regular file through AppleScript or IMAP save | Local data/config corruption and possible code or shell behavior changes | Destination files, availability, integrity | Allowed-root, traversal, and final symlink checks in `resolveAttachmentSaveTarget` | Existing regular files are accepted; IMAP path uses normal `writeFileSync` | Dedicated save root plus exclusive no-overwrite creation for both backends; reject all existing destinations | Log canonical destination, inode/type, and exclusive-create failure | high | high | high |
|
|
122
|
+
| TM-003 | Malicious email or ambiguous client | Numeric id appears in multiple mailboxes/accounts or source mailbox lacks account | Reply, forward, or batch-mutate the first/default match | Wrong-message disclosure or integrity mutation | Mailbox contents, send authority, message placement | `findMessageScript` has scoped and ambiguity-refusing logic; batch source fields exist; id schema limits syntax | `replyToMessage` and `forwardMessage` still walk first match; lone `sourceMailbox` resolves default account | Route every numeric mutation through the shared resolver; require atomic account+mailbox scope; support stable IMAP identity for reply/forward | Record account/mailbox and RFC Message-ID before mutation; refuse multiple hits | high | high | critical |
|
|
123
|
+
| TM-004 | Prompt injection or compromised client | Caller can invoke `create-rule` with delete/move action | Create an enabled persistent rule in one call | Durable autonomous deletion or movement | Persistent rules, mailbox integrity and availability | Tool description asks for confirmation; `disable-rule` and `enable-rule` exist | Confirmation is not enforced; schema and manager default `enabled:true` | Default new rules disabled; require separate explicit enable operation and capability for destructive rule actions | Alert on rule creation, action type, enabled state, and actor identity | medium-high | high | high |
|
|
124
|
+
| TM-005 | Misled client or destructive workflow | IMAP id points to Trash and delete tool is available | Treat recoverable delete as expunge | Permanent mail loss | Mailbox contents and integrity | Non-Trash IMAP deletes move to provider Trash in `trashUids` | Already-Trash branch calls `messageDelete` and reports permanent deletion | Make `delete-message` refuse already-trashed items; add a separate, explicit purge capability only if needed | Alert on attempted purge and record no-op refusal | medium | high | high |
|
|
125
|
+
| TM-006 | Malicious provider or large-message sender | IMAP attachment fetch is enabled | Stream an oversized part to the process | Memory exhaustion or service denial | Server availability and mailbox responsiveness | Inline attachment ceiling protects only outbound base64 in `attachmentLimits.ts` | Closed by #162 — `streamToBuffer` takes an explicit `maxBytes` and throws before the buffer grows past it | Check BODYSTRUCTURE size before download and enforce a streamed byte cap; abort and fail closed | Record declared and observed bytes, account, mailbox, and limit breach | medium | medium-high | medium-high |
|
|
126
|
+
| TM-007 | Network attacker or misconfigured provider | SMTP/IMAP configured without implicit TLS; endpoint can omit STARTTLS | Cause or exploit plaintext continuation | Credential and message disclosure or active tampering | Credentials, mail contents, send authority | SMTP has `secure`; IMAP uses `secure: port === 993`; Node/provider defaults may upgrade opportunistically | STARTTLS is not required explicitly | Set SMTP `requireTLS:true` when not implicit TLS; set ImapFlow `doSTARTTLS:true`; reject unsupported insecure modes | Log negotiated security mode without secrets; fail health checks when TLS cannot be guaranteed | low-medium | high | medium-high |
|
|
127
|
+
| TM-008 | Registry compromise or mutable package drift | Plugin installation uses npm resolution | Start a package different from the reviewed manifest | Full local process authority runs unreviewed code | Build/runtime integrity, Mail, files, credentials | Plugin manifest has a version; package build is committed | `codex/.mcp.json` is exact-pinned (#166); npm resolution still has no integrity/hash binding | Exact version pinned (#166); remaining option is execute a vendored committed artifact with integrity verification | Record resolved package version and hash at install/start; verify against manifest | medium | high | high |
|
|
128
|
+
| TM-009 | Supply-chain change or workflow mistake | Dependabot PR changes a direct dev dependency or generated build | Auto-merge a toolchain change that alters committed `build/` | Unreviewed shipped behavior or malicious artifact promotion | Build artifact, runtime integrity | Rebuild workflow explicitly recognizes esbuild/typescript drift and pins action SHAs | Automerge treats all direct development dependencies as safe | Require human review for npm dependency PRs that can affect shipped bytes; keep action changes separately policy-reviewed | Require changed-file/build hash evidence in PR checks; alert on auto-merge bypass | medium | medium-high | medium |
|
|
129
|
+
|
|
130
|
+
## Finding status for this hardening batch
|
|
131
|
+
|
|
132
|
+
This table records the disposition of the findings covered by the associated hardening PRs. “Closed” means the repository has an enforcement change and focused regression coverage; it does not mean a host wrapper, Mail.app, a provider, or the npm supply chain is trusted.
|
|
133
|
+
|
|
134
|
+
| Threat | Status | Evidence |
|
|
135
|
+
| -------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
136
|
+
| TM-001 — outbound local-file exfiltration | Closed | #168 constrains canonical outbound attachment reads to configured roots and rejects symlink escapes. |
|
|
137
|
+
| TM-002 — inbound attachment overwrite | Closed | #161 uses private staging, exclusive copy/create, and owner-only fallback files. |
|
|
138
|
+
| TM-003 — wrong numeric-message target | Closed | #160 uses recorded location first and refuses unlocated multi-mailbox ambiguity for reads and mutations. |
|
|
139
|
+
| TM-004 — enabled destructive Mail rule | Closed | #164 creates rules disabled unless `enabled: true`; review can happen before `enable-rule`. |
|
|
140
|
+
| TM-005 — empty-Trash expunge | Open follow-up | #163 was intentionally declined because empty-Trash expunge is a documented capability; a separate explicit purge opt-in requires a product decision. |
|
|
141
|
+
| TM-006 — oversized IMAP attachment memory pressure | Closed | #162 enforces the attachment ceiling before buffering and documents the user-visible refusal. |
|
|
142
|
+
| TM-007 — plaintext SMTP/IMAP downgrade | Closed | #171 (continuing #165) requires STARTTLS for non-implicit-TLS transports and documents the explicit plaintext escape hatch. |
|
|
143
|
+
| TM-008 — unpinned runtime package | Open | Exact-pinned by #166; the residual gap is that npm resolution carries no integrity/hash binding. |
|
|
144
|
+
| TM-009 — unreviewed generated-build promotion | Closed | #167's policy change was carried forward and merged as the maintainer continuation #170. |
|
|
145
|
+
|
|
146
|
+
## Criticality calibration
|
|
147
|
+
|
|
148
|
+
- **Critical:** a plausible path can disclose high-sensitivity local or mailbox data, send it externally, permanently destroy mail, or execute unreviewed code with the host's privileges. Examples: TM-001 arbitrary file exfiltration, TM-003 wrong-message send/mutation under broader exposure, and a compromised unpinned runtime package.
|
|
149
|
+
- **High:** a path can durably alter mailbox behavior or corrupt local state, but requires a configured feature or a narrower precondition. Examples: TM-002 regular-file overwrite, TM-004 enabled destructive rules, and TM-005 permanent Trash purge.
|
|
150
|
+
- **Medium:** a path primarily threatens availability or confidentiality under less common configuration or provider behavior. Examples: TM-006 oversized IMAP buffering, TM-007 non-implicit-TLS downgrade, and TM-009 unreviewed build drift.
|
|
151
|
+
- **Low:** a finding that is mainly assurance or maintainability risk with no direct exploit path under the selected deployment. The repository's current security-test duplication and lack of a separate FDA process would be low-to-medium assurance priorities until the deployment confirms those capabilities are enabled.
|
|
152
|
+
|
|
153
|
+
## Focus paths for security review
|
|
154
|
+
|
|
155
|
+
| Path | Why it matters | Related Threat IDs |
|
|
156
|
+
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------ |
|
|
157
|
+
| `src/index.ts` | Tool schemas, routing, send/destructive handlers, rule defaults, and the actual enforcement boundary | TM-001, TM-003, TM-004, TM-005 |
|
|
158
|
+
| `src/services/appleMailManager.ts` | AppleScript authority, numeric-id lookup, attachment paths, and persistent rules | TM-002, TM-003, TM-004 |
|
|
159
|
+
| `src/services/imapClient.ts` | Credential resolution, TLS options, UID mutations, Trash behavior, and attachment streaming | TM-005, TM-006, TM-007 |
|
|
160
|
+
| `src/services/smtpMailer.ts` | Outbound network authority and local attachment reads | TM-001, TM-007 |
|
|
161
|
+
| `src/utils/attachmentMaterialize.ts` | Local file materialization before Mail.app sends | TM-001 |
|
|
162
|
+
| `src/utils/attachmentLimits.ts` | Existing size control that should be shared with IMAP reads | TM-006 |
|
|
163
|
+
| `src/security.test.ts` | Security tests duplicate some production schemas and can drift | TM-001, TM-003 |
|
|
164
|
+
| `codex/.mcp.json` | Exact-pinned (#166); npm resolution still lacks an integrity/hash binding | TM-008 |
|
|
165
|
+
| `.github/workflows/dependabot-automerge.yml` | Automated merge policy can promote unreviewed dependency changes | TM-009 |
|
|
166
|
+
| `.github/workflows/dependabot-rebuild.yml` | Rebuilds and pushes generated runtime bytes with a write token | TM-009 |
|
|
167
|
+
| `SECURITY.md` | Public policy currently describes broad filesystem and confirmation expectations | TM-001, TM-002, TM-004 |
|
|
168
|
+
|
|
169
|
+
## Notes on use
|
|
170
|
+
|
|
171
|
+
This model separates runtime authority from CI/build policy. It treats existing controls as evidence, not as proof that a caller is authorized. The broader-exposure assumption is conditional: the repository itself starts a local stdio transport, so a remote threat exists only if the host or wrapper exposes it; if that wrapper is strictly single-user and authenticated, TM-003 and TM-004 likelihood should be reduced but not removed because prompt injection and malicious email remain in scope. The recommended PR lanes should therefore be reviewed independently, with maximum-security defaults preferred for deployments that expose the MCP beyond one trusted local user.
|
package/package.json
CHANGED