Engineering note
Rewriting a draft in place inside Apple Mail
MailKit gives a compose extension a great deal of metadata and none of the draft. This is how MailTwin replaces the keywords you typed with a finished reply, without disturbing the quoted thread or your signature — and the ways it refuses.
What MailKit will and will not give you
A MEComposeSessionHandler receives a session per compose window, and from it the subject, the sender, the recipients and metadata about the message being replied to. What it does not receive, at any point, is the editable body of the draft. There is no API to read it and none to write it. For an extension whose entire purpose is to rewrite the draft, that is the whole problem.
So the extension does no AI work at all. It performs one job: when the user presses the toolbar button, it writes a session-addressed request into the shared App Group container and posts a Darwin notification. Mail bodies never enter that container. The host app — a separate process, outside Mail's sandbox — does everything else.
Finding the right window
The host now has to locate the exact compose window for that session, and it must be the right one. A user with three reply windows open who gets the wrong one has had their work destroyed, not helped.
The popover the extension presents carries the session UUID as its accessibility identifier. The host walks Mail's accessibility tree for a window containing that identifier. If it finds none, or more than one, it stops. There is deliberately no fallback to "the frontmost window" or to whatever AppleScript reports as selected — a MailKit session that cannot be resolved exactly is abandoned, because a plausible guess is how you write into the wrong draft.
Reading and writing the editor
Mail's compose editor is WebKit. The text is reachable through the accessibility text-marker APIs — AXTextMarkerRangeForUIElement and AXStringForTextMarkerRange — which give the visible body as a string.
The naive move is to replace all of it. That was the first version, and it was wrong in a way that took two shipped builds to fully understand.
Two failures worth describing
Mail's quoted original is a structural <blockquote type="cite">. That structure cannot survive a round trip through NSAttributedString and RTF: the indentation is not a character attribute, so it comes back flattened. Rewriting the whole body therefore destroyed the quote's formatting on every expansion. The fix is not a better serialisation — it is never to write the quote at all. The replaceable region is bounded to the leading span the user actually typed; everything from the quote down is left untouched in Mail's DOM.
The second failure was subtler. Putting an RTF flavour on the pasteboard makes WebKit convert and re-emit styled markup around the insertion point, which disturbed CSS belonging to the quoted mail even when the quote itself was preserved. Pasting plain text inherits the caret's style exactly as typing would, and touches nothing else.
A third, found by a user rather than by us: the signature. A business signature with a whitespace-only line in the middle defeated the heuristic that decides where the signature starts, so the closer and the name were classified as editable body and overwritten. Signature detection now crosses a single interior blank line only when the block above it begins at a recognised sign-off, which is the actual boundary condition.
Refusing to write
Before anything reaches the editor, the expansion passes a local validator. It rejects an empty result, a generated sign-off (Mail appends the account signature itself, so a model-written closer would stack), an invented recipient name or placeholder, any changed URL, email address, number, date or currency amount, and runaway expansion or collapse relative to the input.
There is also a guard that reads the region about to be overwritten and refuses if it contains a sign-off closer in any of the fifteen shipped languages. That exists because the earlier consistency check compared the span against keywords derived from the same signature split — so when the split was wrong, both sides agreed with each other and the write proceeded. A check that shares an input with the thing it is checking is not a check.
The selection is then proven rather than assumed: the host selects the span with the keyboard, reads the selection back, and compares it to what it intended to replace. kAXSelectedTextRangeAttribute is not settable on Mail's WebKit editor, which is why the selection is driven by keystrokes; and the readback is why a mismatch aborts instead of pasting.
Why bother
Every one of these constraints exists because the alternative — a separate window, or a whole replacement email client — is a worse product. People already know how to reply to mail in Mail. The interesting engineering is entirely in the seam.
MailTwin is a menubar app for Apple Mail. Keywords in Mail's own reply window become a finished reply in your own writing style, learned locally per mailbox from your sent mail. Eight AI providers with your own key; Apple Intelligence and Ollama need no key and keep everything on the Mac. No MailTwin server is in the AI path. 19.90 dollars once, 30-day trial, 15 languages, macOS 13 or later. mailtwin.ai