Skip to content

v3.0.0

Latest

Choose a tag to compare

@kiki830621 kiki830621 released this 31 Aug 08:40
· 3 commits to main since this release

Added

  • Self-hosted plugin marketplace — the plugin shell now lives beside its binary (#335). The commands / skills / rules / hooks / wrapper previously lived in the psychquant-claude-plugins aggregator while the Swift source lived here, splitting one product across two repos: archive-mail SOP issues accumulated in the aggregator (#104/#107/#109/#110/#112) while binary issues stayed here (#232/#261), each "split from" the other — and the two manifests drifted (marketplace.json said binary_version: 2.24.0, plugin.json said 2.25.0, while the newest release was v2.26.0, so the v2.44.0 shell shipped an SOP that documents "binary v2.26.0+" against a pin that fetched 2.25.0). Layout follows the che-ical-mcp pilot: a root .claude-plugin/marketplace.json with source: "./plugin", plus a plugin/ subtree carrying the 20 files verbatim (md5-identical, 20/20). The new manifest deliberately omits binary_version so plugin/.claude-plugin/plugin.json is the single source for the binary pin and that axis of the two-manifest drift is structurally impossible rather than merely corrected once. (The shell version is still declared in both manifests — the schema wants one per entry — so ManifestVersionTests pins the pair equal, and the marketplace entry's description deliberately carries no version narrative at all; both guards added at #335 verify.) Cross-references updated in CLAUDE.md and scripts/release.sh; the plugin's own hook suite passes unchanged in its new path (22/22).

Removed — BREAKING

  • The legacy compose path is gone: no code in this project assigns a message
    body through AppleScript any more

    (#304).
    Mail wraps any AppleScript-assigned body in <blockquote type="cite"> at MIME
    serialization. The sender cannot see it — the wrapper's inline style has no
    border, so Apple Mail renders the letter normally — while Gmail's web UI and
    Outlook show the whole thing as quoted text. On 2026-07-29 a formal meeting
    notice went out that way to 10 recipients and could not be recalled; the
    trigger was a cc carrying a Chinese display name, a choice with no visible
    relationship to body rendering. require_wrapper_free already existed and
    already defaulted to false — a default can be flipped back, forgotten, or
    bypassed by the next ineligibility dimension nobody thought of. What changed
    is structural: the eight injection points across four builders were deleted,
    and NoBodyInjectionGuardTests fails the suite if any of the three forms
    (set content, set html content, content: in an outgoing-message
    construction) reappears anywhere under Sources/.

    Removed from the public tool schemas:

    • format: "markdown" and format: "html" on all four composing tools.
      The enum is now ["plain"]. No path this project ships delivers rich text
      without assigning html content, which is the injection this change
      eliminates. That is a statement about what exists, not a proof of
      impossibility
      (#310): the
      clipboard paste path (#218) is a second wrapper-free route already in
      production and NSPasteboard can carry public.rtf / public.html, but
      nobody has checked the MIME such a paste produces —
      #306 is
      settling it. Both values are still parsed, so a caller passing them gets
      an error naming the removal and pointing at #306 and at
      #308 /
      #309 rather
      than a generic "unknown value".
    • require_wrapper_free (compose_email, create_draft, update_draft).
      Its true behavior — fail with a named reason instead of producing a
      wrapped body — is now unconditional, so a flag that is permanently true
      would only imply a control the caller no longer has.
    • sanitize_links. It governed link rendering during markdown-to-HTML
      conversion, which only the composing tools' markdown mode performed. The
      export path is unaffected: batch_export_emails_markdown uses
      EmailMarkdownRenderer, a different module.
    • CHE_MAIL_DISABLE_MAILTO_COMPOSE and CHE_MAIL_DISABLE_PASTE_REPLY.
      Both forced the legacy path; there is nothing left to force.

    Sources/CheAppleMailMCP/MarkdownRendering.swift was deleted with them
    (BodyFormat moved to its own file so format can still be parsed and
    refused by name).

Changed — BREAKING

  • A composing call that cannot use the clean path now FAILS instead of
    silently producing a wrapped body.
    This is the intended behavior change, not
    a regression: calls that previously "succeeded" while quietly degrading will
    now return an error. Failures carry a named reason and an executable
    alternative, and have no side effects — no draft created, nothing sent,
    and (for update_draft) the existing draft left untouched. The set of reasons
    is closed at six and must not be extended by analogy, because every one of
    them is decidable before any side effect: a non-plain format; an empty
    subject; Accessibility not granted (the error names open_mailto, which needs
    no TCC grant but cannot carry attachments); a from_address that is not a
    bare addr-spec; an attachment path containing non-ASCII characters (create the
    draft without attachments and drag the file in — do not rename it to
    ASCII, since the recipient sees that name); and a recipient carrying a display
    name this path cannot fill. Mid-operation failures are a separate contract:
    they propagate, keep their post-dispatch classification
    (#242), and
    never claim that nothing happened.

Capability losses (recorded, not worked around)

Removing the legacy path costs two things that have no replacement:

  • Composing without a visible window. The legacy path was the only way to
    build a message without a compose window appearing; that was the stated reason
    CHE_MAIL_DISABLE_MAILTO_COMPOSE existed ("heavy/unattended automation where
    a briefly-visible compose window is unacceptable", v2.17.0). The mailto:
    hand-off necessarily opens one. If this becomes a real blocker, the route is
    #308 (IMAP
    APPEND) — not reviving the injection.
  • compose_email sending to Name <addr>. Display-name recipients used to
    work by routing to the legacy builder, which set the name natively at the cost
    of the wrapper. The clean path's GUI fill is draft-only
    (#277) — a fill
    that failed on a send would dispatch with missing recipients — so a
    display-name send is now refused. Use create_draft, check the To field,
    and send the draft yourself.

Fixed

  • Attachments added via create_draft / compose_email landed at the start
    of the body instead of the end
    (#341,
    #321 — the same
    defect reported twice from live use). Mail's File ▸ Attach inserts at the
    current insertion point, and after the mailto hand-off fills the body the
    caret sits at its start, so the attachment icon appeared to the LEFT of the
    first line, ahead of the salutation. For a formal letter that is the first
    thing the recipient sees. The script now moves the caret to the end of the
    document (⌘↓, locale-independent like the other shortcuts on this path) once,
    before the first attach cycle.
    Live-verified against real Mail: the pre-fix script produces
    <body …><object …x-apple-msg-attachment… — the attachment first, ahead of
    every line of text — while the fixed script produces
    <body …>FIRST-LINE-MARKER<br><br>--<br>signature line<object …. (MIME part
    order is not the signal: both produce the identical order, because Mail
    serialises attachment parts at the end regardless of visual placement. The
    text/html part is where placement lives.)

  • MailboxURL.mailboxName no longer splits a mailbox name on its own slash (#358). It took the substring after the last / of mailboxPath, which is the lossy whole-path decode — so a mailbox literally named R&D/Sent (raw R%26D%2FSent) reported its leaf as Sent. Exactly the defect #344 removed from the mailbox filter, still standing in the accessor next door. It now derives from pathComponents. It had zero consumers, which is why it was fixed rather than deleted: it reads naturally enough that removing it would invite the next caller to write mailboxPath.split("/").last themselves and reintroduce the bug in their own code.