Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@
},
{
"name": "che-apple-mail-mcp",
"version": "2.18.1",
"description": "Apple Mail MCP server — 47 tools, reply-as-draft + IDD task enforcement + NSQL confirmation + .claude/.mail/ namespace (shell v2.18.1, binary v2.7.2)。Shell v2.18.1 ships binary v2.7.2: #71 get_email_metadata SQLite fallback parity (last of 8 read tools to gain do/catch wrapper) + cluster #61-64 attachmentFragment hardening (indent normalization, dead-helper deletion, 50-attachment count cap, env-configurable CHE_MAIL_ATTACHMENT_DELAY_BETWEEN/_TRAILING delays)。疊加 v2.18.0 (#76) Staleness Detection — wrapper writes runtime state file on spawn (pid/started_at/version_at_spawn);新增 hooks/session-start.sh,session 啟動時偵測 plugin.json 與 in-memory binary 版本 drift,SIGTERM(+5s grace,SIGKILL fallback)stale PID 讓 Claude Code 重 spawn 取新 binary,徹底解決「plugin update 後當下 session 仍跑舊 binary」silent staleness 問題。疊加 v2.17.0 (#49) Workspace Layout Detection;v2.16.1 ship 鏈到 binary v2.7.1 (#72 base64 fix + #69 SQLite fast-path stderr logging + #66 .partial.emlx attachment fix);v2.16.0 config schema .md → .yaml (#47);v2.15.0 inline cid: 圖片保留 (#45);v2.14.0 dedup_strategy opt-in (#18);v2.13.0 markdown template 簡化 (#17);v2.12.0 zero-arg mode (#13/#21);v2.11.0 binary v2.7.0 multi-attachment race fix (#60);v2.6.0 marathon (8 PRs);v2.9.0 task enforcement;v2.8.0 namespace;v2.7.0 NSQL confirmation。",
"version": "2.19.0",
"description": "Apple Mail MCP server — 48 tools, reply-as-draft + IDD task enforcement + NSQL confirmation + .claude/.mail/ namespace (shell v2.19.0, binary v2.8.0)。Shell v2.19.0 ships binary v2.8.0: (#19) `sanitize_links` opt-in URL scheme allowlist for markdown mode (defends against `[click](javascript:alert('xss'))` and `data:`/`file:`/`vbscript:` XSS vectors with closed allowlist `{http, https, mailto, tel}`; default `false` preserves backwards compat) + (#85) formal `openspec/specs/message-composition/spec.md` Requirement+Scenarios codifying the allowlist contract + builder-layer wiring contract tests pinning `sanitizeLinks` forwarding across `buildComposeEmailScript/buildCreateDraftScript/buildReplyEmailScript/buildForwardEmailScript` + cluster A hygiene (#20 dead spec scenario delete / count-free CHANGELOG / `assertOrdered` helper, #21 reply/forward AppleScript-html-denial docs, #25 `list_attachments_batch` SQLite+.emlx cross-validation parity with single-message handler, #27 `EmlxParser.attachmentNames` <200ms latency budget regression test, #32 `attachmentNames`↔`saveAttachment` matcher parity invariant test across 4 fixtures) + (#73) `extractHTMLBody` base64+UTF-8 quoted-printable decoding fixes。疊加 v2.18.1 (#71 get_email_metadata SQLite fallback parity + cluster #61-64 attachmentFragment hardening);v2.18.0 (#76) Staleness Detection — wrapper writes runtime state file on spawn (pid/started_at/version_at_spawn);新增 hooks/session-start.sh,session 啟動時偵測 plugin.json 與 in-memory binary 版本 drift,SIGTERM(+5s grace,SIGKILL fallback)stale PID 讓 Claude Code 重 spawn 取新 binary,徹底解決「plugin update 後當下 session 仍跑舊 binary」silent staleness 問題。疊加 v2.17.0 (#49) Workspace Layout Detection;v2.16.1 ship 鏈到 binary v2.7.1 (#72 base64 fix + #69 SQLite fast-path stderr logging + #66 .partial.emlx attachment fix);v2.16.0 config schema .md → .yaml (#47);v2.15.0 inline cid: 圖片保留 (#45);v2.14.0 dedup_strategy opt-in (#18);v2.13.0 markdown template 簡化 (#17);v2.12.0 zero-arg mode (#13/#21);v2.11.0 binary v2.7.0 multi-attachment race fix (#60);v2.6.0 marathon (8 PRs);v2.9.0 task enforcement;v2.8.0 namespace;v2.7.0 NSQL confirmation。",
"author": {
"name": "Che Cheng"
},
"source": "./plugins/che-apple-mail-mcp",
"category": "productivity"
"category": "productivity",
"binary_version": "2.8.0"
},
{
"name": "che-apple-notes-mcp",
Expand Down
6 changes: 3 additions & 3 deletions plugins/che-apple-mail-mcp/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "che-apple-mail-mcp",
"version": "2.18.1",
"binary_version": "2.7.2",
"description": "Apple Mail MCP server — 47 tools, reply-as-draft + IDD task enforcement + NSQL confirmation + .claude/.mail/ namespace (shell v2.18.1, binary v2.7.2)。Shell v2.18.1 ships binary v2.7.2: #71 get_email_metadata SQLite fallback parity (last of 8 read tools to gain do/catch wrapper) + cluster #61-64 attachmentFragment hardening (indent normalization, dead-helper deletion, 50-attachment count cap, env-configurable CHE_MAIL_ATTACHMENT_DELAY_BETWEEN/_TRAILING delays)。疊加 v2.18.0 (#76) Staleness Detection — wrapper writes runtime state file on spawn (pid/started_at/version_at_spawn);新增 hooks/session-start.sh,session 啟動時偵測 plugin.json 與 in-memory binary 版本 drift,SIGTERM(+5s grace,SIGKILL fallback)stale PID 讓 Claude Code 重 spawn 取新 binary,徹底解決「plugin update 後當下 session 仍跑舊 binary」silent staleness 問題。疊加 v2.17.0 (#49) Workspace Layout Detection;v2.16.1 ship 鏈到 binary v2.7.1 (#72 base64 fix + #69 SQLite fast-path stderr logging + #66 .partial.emlx attachment fix);v2.16.0 config schema .md → .yaml (#47);v2.15.0 inline cid: 圖片保留 (#45);v2.14.0 dedup_strategy opt-in (#18);v2.13.0 markdown template 簡化 (#17);v2.12.0 zero-arg mode (#13/#21);v2.11.0 binary v2.7.0 multi-attachment race fix (#60);v2.6.0 marathon (8 PRs);v2.9.0 task enforcement;v2.8.0 namespace;v2.7.0 NSQL confirmation。",
"version": "2.19.0",
"binary_version": "2.8.0",
"description": "Apple Mail MCP server — 48 tools, reply-as-draft + IDD task enforcement + NSQL confirmation + .claude/.mail/ namespace (shell v2.19.0, binary v2.8.0)。Shell v2.19.0 ships binary v2.8.0: (#19) `sanitize_links` opt-in URL scheme allowlist for markdown mode (defends against `[click](javascript:alert('xss'))` and `data:`/`file:`/`vbscript:` XSS vectors with closed allowlist `{http, https, mailto, tel}`; default `false` preserves backwards compat) + (#85) formal `openspec/specs/message-composition/spec.md` Requirement+Scenarios codifying the allowlist contract + builder-layer wiring contract tests pinning `sanitizeLinks` forwarding across `buildComposeEmailScript/buildCreateDraftScript/buildReplyEmailScript/buildForwardEmailScript` + cluster A hygiene (#20 dead spec scenario delete / count-free CHANGELOG / `assertOrdered` helper, #21 reply/forward AppleScript-html-denial docs, #25 `list_attachments_batch` SQLite+.emlx cross-validation parity with single-message handler, #27 `EmlxParser.attachmentNames` <200ms latency budget regression test, #32 `attachmentNames`↔`saveAttachment` matcher parity invariant test across 4 fixtures) + (#73) `extractHTMLBody` base64+UTF-8 quoted-printable decoding fixes。疊加 v2.18.1 (#71 get_email_metadata SQLite fallback parity + cluster #61-64 attachmentFragment hardening);v2.18.0 (#76) Staleness Detection — wrapper writes runtime state file on spawn (pid/started_at/version_at_spawn);新增 hooks/session-start.sh,session 啟動時偵測 plugin.json 與 in-memory binary 版本 drift,SIGTERM(+5s grace,SIGKILL fallback)stale PID 讓 Claude Code 重 spawn 取新 binary,徹底解決「plugin update 後當下 session 仍跑舊 binary」silent staleness 問題。疊加 v2.17.0 (#49) Workspace Layout Detection;v2.16.1 ship 鏈到 binary v2.7.1 (#72 base64 fix + #69 SQLite fast-path stderr logging + #66 .partial.emlx attachment fix);v2.16.0 config schema .md → .yaml (#47);v2.15.0 inline cid: 圖片保留 (#45);v2.14.0 dedup_strategy opt-in (#18);v2.13.0 markdown template 簡化 (#17);v2.12.0 zero-arg mode (#13/#21);v2.11.0 binary v2.7.0 multi-attachment race fix (#60);v2.6.0 marathon (8 PRs);v2.9.0 task enforcement;v2.8.0 namespace;v2.7.0 NSQL confirmation。",
"author": {
"name": "Che Cheng"
},
Expand Down
1 change: 1 addition & 0 deletions plugins/che-apple-mail-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ https://github.com/kiki830621/che-apple-mail-mcp

## Version History

- **v2.19.0 shell + binary v2.8.0**(2026-05-11)— **`sanitize_links` security feature + formal spec coverage** ([release notes](https://github.com/PsychQuant/che-apple-mail-mcp/releases/tag/v2.8.0)). Opt-in URL scheme allowlist for markdown mode ([#19](https://github.com/PsychQuant/che-apple-mail-mcp/issues/19)) defends against `[click](javascript:alert('xss'))` and `data:`/`file:`/`vbscript:` URLs via closed allowlist `{http, https, mailto, tel}`; default `false` preserves backwards compat. Formal `openspec/specs/message-composition/spec.md` Requirement+Scenarios codifying the contract + builder-layer wiring contract tests ([#85](https://github.com/PsychQuant/che-apple-mail-mcp/issues/85)) pinning `sanitizeLinks` forwarding across the 4 script-builder functions. Cluster A hygiene: dead spec scenario delete + count-free CHANGELOG + `assertOrdered` helper ([#20](https://github.com/PsychQuant/che-apple-mail-mcp/issues/20)); reply/forward AppleScript-html-denial docs ([#21](https://github.com/PsychQuant/che-apple-mail-mcp/issues/21)); `list_attachments_batch` SQLite+.emlx cross-validation parity ([#25](https://github.com/PsychQuant/che-apple-mail-mcp/issues/25)); `attachmentNames` <200ms latency budget test ([#27](https://github.com/PsychQuant/che-apple-mail-mcp/issues/27)) + parity invariant ([#32](https://github.com/PsychQuant/che-apple-mail-mcp/issues/32)). `extractHTMLBody` base64+UTF-8-QP decoding fixes ([#73](https://github.com/PsychQuant/che-apple-mail-mcp/issues/73)). 48 tools (was 47). swift test → 313/0/8 (was 309).
- **v2.10.3 shell + binary v2.6.0**(2026-05-03)— **Marathon release: 16 issues across 8 PRs** ([release notes](https://github.com/PsychQuant/che-apple-mail-mcp/releases/tag/v2.6.0))。安全強化:id 注入防護 ([#50](https://github.com/PsychQuant/che-apple-mail-mcp/issues/50))、附件路徑 deny-list + opt-in `MAIL_MCP_ATTACHMENT_ROOTS` allow-list ([#38](https://github.com/PsychQuant/che-apple-mail-mcp/issues/38))、email 地址驗證 ([#41](https://github.com/PsychQuant/che-apple-mail-mcp/issues/41))、type-strict handler params ([#35](https://github.com/PsychQuant/che-apple-mail-mcp/issues/35))、新 SECURITY.md ([#48](https://github.com/PsychQuant/che-apple-mail-mcp/issues/48))。Bug fix:`forward_email` plain mode 也嵌入 quoted original ([#44](https://github.com/PsychQuant/che-apple-mail-mcp/issues/44))。Quality:cc_additional case-insensitive dedup ([#34](https://github.com/PsychQuant/che-apple-mail-mcp/issues/34))、schema test type assertions ([#42](https://github.com/PsychQuant/che-apple-mail-mcp/issues/42))、indent cleanup ([#39](https://github.com/PsychQuant/che-apple-mail-mcp/issues/39))、README updates ([#36](https://github.com/PsychQuant/che-apple-mail-mcp/issues/36))、large-thread script size tests ([#49](https://github.com/PsychQuant/che-apple-mail-mcp/issues/49))。Tests:gated integration tests for reply runtime ([#37 + #45](https://github.com/PsychQuant/che-apple-mail-mcp/issues/37))。Smoke matrix templates ([#46 + #47](https://github.com/PsychQuant/che-apple-mail-mcp/issues/46))。**279 tests pass** (+45 from 234)。
- **v2.10.2 shell + binary v2.5.0**(2026-05-03)— **Reply quoted-original fix** ([issue #43](https://github.com/PsychQuant/che-apple-mail-mcp/issues/43))。`reply_email` plain mode 終於把 quoted original 嵌入 draft body — 自 `b8a4a89`(initial release)以來每次 plain reply 都靜默 drop 掉 quoted original,因為 AppleScript `& content` 對 freshly-created outgoing message 讀為空。改用 Swift-side `composeReplyPlainText` helper 預先 fetch + RFC 3676 `> ` prefix 組合 quoted body。Round-1 hardening 涵蓋 CRLF/CR normalization、trailing newline trim、空行 stuffing(`>` 不含 trailing space)、pre-fetch 失敗 graceful degrade。**Wire-output 行為改變**:plain reply body 從 `<user reply>` 變成 `<user reply>\n\n> <quoted lines>`。HTML branch 不變(原本就走對的 architecture)。234 tests pass。Follow-ups #44–#50 已開。
- **v2.10.1 shell + binary v2.4.1**(2026-05-02)— **Save-as-draft popup + path validation fix** ([issue #33 verify findings A+B](https://github.com/PsychQuant/che-apple-mail-mcp/issues/33))。`reply_email` `save_as_draft=true` 不再彈出 Mail.app 視窗(windowClause 改成條件式 with/without opening window),`replyEmail` 加 `validateFilePaths` 鏡像 composeEmail / createDraft 行為。
Expand Down
16 changes: 12 additions & 4 deletions plugins/che-apple-mail-mcp/commands/archive-mail.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,16 +332,24 @@ mkdir -p "${output_dir}" # archive markdown 目的地(不變)
當 `${output_dir}` 下有 symlinked subdirectory(典型情境:transitioned-project pattern,例如 chchen_lab 把 `email/application/` symlink 到 `applications/completed/.../emails/`),掃描其下 markdown 的 YAML frontmatter,把 `message_id:` 值併入 in-memory dedup set。**讀取 only,從不寫入 symlink target**。

```bash
# === INVARIANT: this block is READ-ONLY w.r.t. $symlink_dir ===
# Allowed: find / head / awk / cat / grep
# FORBIDDEN: mv / rm / cp / > / >> / tee / chmod (against $symlink_dir or its target)
# If you need to write, do it OUTSIDE this block.
# (v2.17.x #56 — invariant marker; future hardening: bash trap-based check or pre-commit scan,
# both deferred. This comment is the load-bearing contract for future maintainers.)

# Only run when dedup_strategy uses index (skip if last_archived only)
if [ "$DEDUP_STRATEGY" = "index" ] || [ "$DEDUP_STRATEGY" = "both" ]; then
# Find symlinked subdirectories in output_dir (1 level deep)
EXTENDED_COUNT=0
EXTENDED_SOURCES=()
while IFS= read -r symlink_dir; do
# Null-safe: -print0 + read -d '' handles filenames containing newlines / unusual chars (#57)
while IFS= read -r -d '' symlink_dir; do
[ -z "$symlink_dir" ] && continue
# Bound search depth + use -P to NOT follow symlinks recursively (we follow once into the symlinked dir, then stay)
ENTRIES_THIS_DIR=0
while IFS= read -r mdfile; do
while IFS= read -r -d '' mdfile; do
[ -z "$mdfile" ] && continue
# Read just the YAML frontmatter (first ~30 lines is generous)
mid=$(head -30 "$mdfile" 2>/dev/null | awk '/^message_id:[ \t]*/{sub(/^message_id:[ \t]*"?/,"");sub(/"?$/,"");print;exit}')
Expand All @@ -351,13 +359,13 @@ if [ "$DEDUP_STRATEGY" = "index" ] || [ "$DEDUP_STRATEGY" = "both" ]; then
EXTENDED_DEDUP_IDS+=("$mid")
ENTRIES_THIS_DIR=$((ENTRIES_THIS_DIR + 1))
fi
done < <(find -P "$symlink_dir/" -maxdepth 2 -name "*.md" -type f 2>/dev/null)
done < <(find -P "$symlink_dir/" -maxdepth 2 -name "*.md" -type f -print0 2>/dev/null)

if [ "$ENTRIES_THIS_DIR" -gt 0 ]; then
EXTENDED_COUNT=$((EXTENDED_COUNT + ENTRIES_THIS_DIR))
EXTENDED_SOURCES+=("$symlink_dir ($ENTRIES_THIS_DIR entries)")
fi
done < <(find -P "${output_dir}" -maxdepth 1 -type l 2>/dev/null)
done < <(find -P "${output_dir}" -maxdepth 1 -type l -print0 2>/dev/null)

if [ "$EXTENDED_COUNT" -gt 0 ]; then
echo "🔗 Extended dedup with $EXTENDED_COUNT entries from sibling archives:"
Expand Down
21 changes: 21 additions & 0 deletions plugins/plugin-tools/skills/plugin-update/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ if [ -z "${TARGET_PLUGIN:-}" ]; then
fi

# 收集 unpushed commits touch 到的 plugin 名(去重)
# Note (#70): 以 inline subshell `$(...)` capture 到 process-local 變數,
# **不**經過 /tmp tempfile,故無 TOCTOU / shared-/tmp race condition。
# 若未來重構需要持久化 list,改用 `mktemp -t plugin-update-touched.XXXXXX`
# + `trap "rm -f \$TMPFILE" EXIT` 而**禁用**hardcoded /tmp/touched-plugins.txt。
TOUCHED=$(git log --name-only --pretty=format: "$UPSTREAM"..HEAD \
| grep '^plugins/' | cut -d/ -f2 | sort -u)
TOUCHED_COUNT=$(echo -n "$TOUCHED" | grep -c . || true)
Expand Down Expand Up @@ -407,6 +411,12 @@ options:
| `plugin-deploy` Step 2.5 | 偶爾(發版時)| **BLOCK** | Release 沒 binary = 新使用者裝 plugin 就壞,不能放過 |
| `plugin-update` Phase 1.5 | 頻繁(日常同步)| **ASK + AUTO-SYNC** | 開發者通常想要一次更新完,但要尊重「只改 shell 不動 binary」的情境 |

> **Default-option policy alignment with Phase 0.5**(audit per #68):
>
> Phase 0.5 rule:**`abort` is the default for any state with multiple sensible actions;active default is reserved for unambiguous happy-path / reversible action / frequent dev flow**(per Phase 0.5 Step 3 explicit rule)。
>
> Phase 1.5 default 「順便更新」**符合 exception clause** — binary-out-of-sync 在 daily dev flow 是 unambiguous happy path(開發者通常確實想要 binary + shell 一起更新),而且 binary install 是 reversible(可重跑 mcp-deploy / cli-upgrade 換版本)。本 phase 跟 Phase 0.5 的 abort-default 不衝突,各自服務不同 lifecycle moment(release-time push 不可逆 vs dev-time binary sync 可逆)。

### Step 5: 執行 auto-sync(若使用者選擇)

**MCP 情境**:
Expand Down Expand Up @@ -680,6 +690,17 @@ options:
| `plugin-update` Phase 2.5 | 頻繁(日常同步)| **ASK** | 有時純修 typo / hook / internal refactor,不需要動 README |
| `plugin-deploy` Step 2 | 偶爾(發版時)| **列入 checklist 並 offer 修復** | 正式發布時使用者第一眼看 README,stale 就是差的第一印象 |

> **Default-option policy alignment with Phase 0.5**(audit per #68):
>
> Phase 0.5 rule(canonical 3-clause):**active default 保留給 unambiguous happy-path / reversible action / frequent dev flow**(per Phase 0.5 Step 3 explicit rule)。
>
> Phase 2.5 default 「更新 README」**符合 exception clause(2/3 criteria + substitution)**:
> - **unambiguous happy-path** ✓ — 偵測到 README 與當前 plugin.json version 不同步,起草更新是 unambiguous 的方向(scope 是「重新生成 stale 段落」,不是 design choice)
> - **reversible action** ✓ — 該選項不直接 mutate README,而是 propose diff(read CHANGELOG + git log → 起草 → user 審閱後才 commit);User-confirmation gate 還在,只是把 draft 起草的 friction 從 user 轉到 skill
> - **frequent dev flow** ✗ → **substituted with non-destructive** — README update 並非 daily dev flow(只在 substantive change 後 fire),故 frequent-dev criterion 不適用;以 non-destructive(propose-only,不 mutate disk)替代,作為 risk-profile 等價的 fallback criterion
>
> 對比 Phase 0.5 abort-default 的 `git push` 情境:push to public marketplace 是 irreversible(回滾需要 force-push,在 public branch 是 bad day),所以即使用戶在 keyboard 也不該 default push。各 phase 的 default 反映其 action 的 risk profile。

---

## Phase 3: 同步 Marketplace Cache
Expand Down
Loading