Engineering note

Four ways Apple Mail hides your Sent mailbox

MailTwin learns your writing style by reading your sent mail once, locally. To do that it has to find the sent mailbox. That sounds like one line of AppleScript. It produced four separate shipped bugs, three of which were invisible to every test we had, and each one was found by a user rather than by us.

1. The name is translated

The obvious implementation matches the mailbox name against Sent, Sent Messages, Sent Items. That is what we shipped, and it works perfectly in English.

Mail localizes that name. On a Spanish Mac the mailbox is Enviados; German, Gesendet; Japanese, 送信済み. So for every user running Mail in one of the other fourteen languages we ship in, style learning found nothing at all — and reported no error, because there was nothing to report. There was simply no mailbox by that name, and an empty result is not a failure.

A reviewer in Spain found it. The fix is a name table across all fifteen shipped locales, plus the path forms mail servers use, like [Gmail]/Sent Mail and INBOX.Enviados. Matching is exact rather than substring, so a user's own mailbox called "Sent invoices" does not qualify.

2. The real folder isn't in the list

That fix was correct and still insufficient, which took a second field report to understand.

On a real Exchange account, name of every mailbox of acc did not include the account's live sent folder at all. Mail hides special folders from that collection while still resolving them by direct address. The enumeration returned 125 mailboxes; the one holding 31,398 sent messages was not among them. What the localized name-matching did find, on that same account, was a folder called "Sendt" — a personal archive whose newest message was from 2009.

So the search order now asks for the canonical names by direct address first — mailbox "Sent Items" of acc — and only falls back to scanning the enumerated list. Alias resolution deterministically prefers the true special folder, which is exactly the behaviour you want and cannot discover by enumerating.

3. A name specifier can change its mind

Having found a mailbox by name, the natural next step is to keep using that name to read from it. Under AppleScript that is not a stable reference: the specifier is re-resolved on the far side, and it can select a different same-named mailbox between two Apple Events. We observed a binding to "Sendt" become Old/Protectas/Sendt partway through a read.

Names found in the enumerated list are therefore bound by index for the life of the script; direct address is reserved for the hidden special folders that index binding cannot reach. Ambiguity — two mailboxes matching, or zero — aborts the read rather than picking one.

There is a related ordering trap. The special folder lists newest first; an ordinary archived mailbox lists oldest first. A blind ascending read of the wrong one teaches the model how its owner wrote in 2005. Each batch now compares the first and last message dates once and reads newest-first either way.

4. A subprocess per mailbox, inside a ten-second budget

The localized matching needed case-insensitive comparison, and the implementation reached for do shell script "printf … | tr" — one sh, one printf, one tr, per mailbox examined.

On the 125-mailbox account that was up to 125 process spawns inside a script with a ten-second timeout. Discovery blew the budget and aborted the entire re-index before a single message was read. The user saw the operation fail and reasonably concluded the earlier fix had not worked.

AppleScript's is comparison already ignores case and considers diacritics, so the subprocesses were never needed. With them gone and mailbox names fetched in one event per account, resolving four real accounts takes 0.15 seconds — down from a guaranteed timeout.

The one that was worst

There is a fifth, and it is the one I would most like back. The app has a "Diagnose sent mailboxes" button, which exists precisely so a user whose indexing finds nothing can see which mailboxes MailTwin can reach.

When the localized matching landed, that diagnostic was pointed at the new shared routine — but the routine was never included in the diagnostic's own script. AppleScript raised an undefined-handler error, the enclosing try swallowed it, and the button answered (none found) for every account regardless of reality. For three builds, the screen you open when nothing works told you nothing works.

Fail-closed is usually right. Here it produced a confident, specific, wrong answer on the one surface whose entire job is to be trustworthy when everything else is suspect. A try that spans a call to your own code is not error handling; it is a place for bugs to go and be quiet.

What the tests were worth

None of these five were caught by our test suite, and the reason is uniform: every one lives in the gap between our code and Mail's actual behaviour, which unit tests do not cross. What we have added instead is cheap and specific — a test that every AppleScript handler called is also defined in the same script (it immediately found a second broken script), assertions that discovery contains no subprocess spawn and probes canonical names before scanning, and a check that batch reads are direction-aware. Those are guards against the exact mistakes already made, which is the only kind of test that was ever going to help here.

The rest of the answer is less satisfying and more honest: this class of bug is found by users, so the job is to make it cheap for them to report and fast for us to correct. Two screenshots from one patient reviewer produced three of the five fixes above.

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 · Inside MailKit