Cupertino 1.23.0
Version 1.23.0 ·
Changed
apple_mail_update_draftnow edits a reply draft instead of refusing it. Revising a draft is the most common thing asked of this server — "draft a reply, now change this line" — and it was the one thing the tool would not do. Rewriting a draft meant RECREATING it, which cannot carryIn-Reply-Toacross, so a reply draft was refused outright and the only way through was deleting it in Mail by hand.Recreating was never the only option; it was the only one reachable from Apple Events. The draft's own composer window is still on screen — nothing in this server closes one — and a body replaced in that window saves back to the same draft, so threading, attachments and the quoted original all survive because nothing is remade.
update_draftnow does that first and falls back to recreating only when no composer is open. The result says which route it took inmethod, and a ref stays valid across an in-place edit.Telling the draft's own text from the message it quotes is the whole difficulty, and
AXBlockQuoteLevelanswers it exactly: the sender's paragraphs read 0, and the attribution line and everything below it read 1 — for a forward as well as a reply, which was measured rather than assumed. The selection is then proved before anything is replaced: it is copied back and must equal the composer's own text exactly, becauseAXSelectedTextreads null on Mail's composer and the pasteboard is the only place a selection is legible. Selecting and copying change nothing, so a selection that cannot be made to match costs a refusal rather than somebody's draft. Measured on macOS 27.0 (build 26A428) and verified end to end against a live Mail on a forward draft — the kind recreation refuses — which came back with one draft, itsReferencesheader intact and the forwarded message still under the new text.docs/mail-compose.mdcarries the sequence and the readings, andnode scripts/verify-mail-ax.mjs --composere-runs the whole thing.
Fixed
Opening or resizing a window could abort the app. Diagnosed from a crash report: naming a window for AppKit's frame autosave makes it write the frame out from inside
-[NSWindow _setFrameCommon:]— so a resize SwiftUI itself performs during the window's own layout pass persists a frame, persisting postsNSUserDefaultsDidChange, an@AppStorageobserver reads that as a settings change and dirties the hosting view, and thesetNeedsUpdateConstraintsthat follows lands inside the layout pass that is still running. AppKit throws rather than re-enter, nobody catches it, and the process takes SIGABRT. It needs no bad frame and no bad window: one@AppStorageanywhere in the app is fuel enough. The frame is still remembered, but it is read withsetFrameUsingNameand written a turn later by a saver that only believes a person's own resize or move — never a size SwiftUI tried on its own. The key and its format are unchanged, so a frame saved by an earlier build still restores.Every accent, em dash and curly quote in a composed mail was corrupted.
pbcopyandpbpasteencode in the LOCALE's character set, and this server is spawned by the app with an environment naming none — so they fell back to MacRoman, and every byte outside ASCII was wrong in both directions. A body pasted into a composer arrived with—turned into‚Äî. Not a display artefact: that is what went into the draft, and what would have gone into the mail. The read is worse in kind, because the clipboard is BORROWED — read, overwritten, put back — so replying to a mail handed back a corrupted copy of whatever the person had copied. Both verbs now nameLC_CTYPE=UTF-8, which sets the encoding and nothing else. Found by a live rewrite whose read-back would not match, and verified end to end:é à ç œ « »and—now reach the stored draft intact.A mailbox holding fewer messages than the limit could not be listed at all.
apple_mail_list_messagescame back with a null subject, null sender and an unusable#nullref on every row — so nothing else could act on any of them.slice(from, to)on an Apple Events specifier is INCLUSIVE ofto, not exclusive like JavaScript's, so asking fornmessages asked forn + 1; whennwas the whole mailbox that ran one past the end, raisedInvalid index., and left every batched property read empty. Drafts is where it was found, because a Drafts mailbox almost always holds fewer messages than the limit — an agent that had just written a reply could not find the draft again to revise it. The same off-by-one was inSENT_SINCE, where it matters most: that is the read deciding whether a reply was SENT, and on a Sent mailbox smaller than the limit it answered "it does NOT appear to have been sent" for a mail that had gone, inviting a second send. Measured against a live Mail:slice(0, 303)on a 303-message mailbox raises,slice(0, 302)returns all 303.The stubs had hidden it by being kinder than Mail — they modelled
Array.prototype.slice, which is exclusive and forgiving of an index past the end. They now raise where Mail raises, and 7 of the 9 existingsent-sincetests fail without the fix.apple_mail_reply_to_messageandapple_mail_forward_messagereported a correct draft as a failure. The composer was read back once, immediately after the paste — butapple_desktop_keyreturns when the keystroke is POSTED, and WebKit has still to take it, edit the document and republish an accessibility tree. On a long message the read lost that race and the reply came backbodyVerified: falsewith "SOMETHING DID land in it that could not be read back", for a body that was in fact perfect. Worse, that message tells its reader not to retry. Measured against a reply quoting a 322-element newsletter. The read-back is now polled, the same way the focus poll above it already was.