<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paris Moschovakos</title>
    <description>The latest articles on DEV Community by Paris Moschovakos (@paris_moschovakos_5f8f1e0).</description>
    <link>https://dev.to/paris_moschovakos_5f8f1e0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4096999%2F839d3e27-b697-4996-8fe1-29ef99ea2741.jpg</url>
      <title>DEV Community: Paris Moschovakos</title>
      <link>https://dev.to/paris_moschovakos_5f8f1e0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paris_moschovakos_5f8f1e0"/>
    <language>en</language>
    <item>
      <title>One SQLite query took Apple Mail automation from 10s to 0.1ms</title>
      <dc:creator>Paris Moschovakos</dc:creator>
      <pubDate>Thu, 27 Aug 2026 08:36:32 +0000</pubDate>
      <link>https://dev.to/paris_moschovakos_5f8f1e0/one-sqlite-query-took-apple-mail-automation-from-10s-to-01ms-2e35</link>
      <guid>https://dev.to/paris_moschovakos_5f8f1e0/one-sqlite-query-took-apple-mail-automation-from-10s-to-01ms-2e35</guid>
      <description>&lt;p&gt;I run my work email through an AI assistant. The mailbox is a CERN mailbox, 298,986 messages across an Exchange account and a personal one. I build control systems for the ATLAS detector for a living, so when the assistant needed ten seconds to touch a single message it had already identified, I treated it like any other misbehaving system and went looking for the bottleneck.&lt;/p&gt;

&lt;p&gt;On a two thousand message inbox none of this matters and you should close the tab. On mine, the bottleneck turned out to be architectural. The fix turned out to be one query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Every Apple Mail automation tutorial reaches for the same AppleScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mail"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nb"&gt;first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;inbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;whose&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a small mailbox this feels fine. On my 71,344-message inbox it measures 7 to 10 seconds. Not to search for anything. To address one message whose identity I already had in hand. The first time I hit this, on an older version of Mail, the same call took 85 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dead end
&lt;/h2&gt;

&lt;p&gt;My first assumption was that I was holding the whose clause wrong. Scope it to one mailbox, filter by date first, ask for less data back. None of it moves the needle, and once you understand what a whose clause is, you see why it never will.&lt;/p&gt;

&lt;p&gt;A whose clause walks the collection element by element, and every step is an Apple event, an interprocess round trip between your script and the Mail process. It cannot be indexed and it cannot be made fast. Narrowing the filter shortens a walk that is still a walk. Every route through the scripting interface hits the same wall, because the wall is the interface itself.&lt;/p&gt;

&lt;p&gt;Which means the standard advice scales exactly backwards. The people who most need mail automation, the ones drowning in volume, are the ones for whom it performs worst.&lt;/p&gt;

&lt;h2&gt;
  
  
  One query
&lt;/h2&gt;

&lt;p&gt;Mail keeps its entire envelope catalog in a SQLite database on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Mail/V10/MailData/Envelope Index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I opened it out of curiosity more than hope. The &lt;code&gt;messages&lt;/code&gt; table holds everything a mail client shows in a list view. Sender, subject, date received, mailbox, read flags. And a ROWID per message, which is what SQLite gives every table for free.&lt;/p&gt;

&lt;p&gt;Then I asked Mail's scripting interface for the id of a message I had just pulled out of that table, and it was the same number.&lt;/p&gt;

&lt;p&gt;That is the entire discovery. The ROWID in the Envelope Index is the id that Mail exposes on its scriptable message objects. So the whose clause is optional. You find the message with an indexed SQLite query, then address it directly by id in a single AppleScript call at the moment you actually need Mail to act on it. AppleScript stops being the query engine, which it is catastrophically bad at, and becomes the actuator, which it is fine at. SQLite does what SQLite does.&lt;/p&gt;

&lt;p&gt;Same operation as the whose clause. 0.1 ms median instead of 7 to 10 seconds.&lt;/p&gt;

&lt;p&gt;I have not found this correspondence documented anywhere. It has held across every message and both accounts in my store, and the id survives as a stable handle for the message's lifetime in the index. If you know of a place Apple guarantees it, or a case where it breaks, I want to hear about it.&lt;/p&gt;

&lt;p&gt;No daemon, no cache to keep warm, no reimplementing IMAP. The index was there the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reads never need AppleScript at all
&lt;/h2&gt;

&lt;p&gt;Once envelope data comes from SQLite, whole categories of work stop touching Mail entirely. Listing, filtering, threading, counting unread. All of it is read-only queries against a database that Mail maintains for you.&lt;/p&gt;

&lt;p&gt;For full-text search I keep a separate FTS5 index of message bodies. Queries over the full store, 299k documents, come back with p95 under a millisecond. That is fast enough that the assistant can afford to be wasteful, running five speculative searches to answer one question, which turns out to be exactly how assistants like to work.&lt;/p&gt;

&lt;p&gt;There is a catch on Exchange. Mail downloads bodies lazily, so older messages often have no local body to index, and the index ends up full of holes exactly where an assistant needs history. The fix is a backfill that fetches the missing bodies from the server side into the index. In my store that recovered 109,563 bodies Mail had never downloaded. Roughly a third of my archive was invisible to local search until then, and I had no idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that made me paranoid about sends
&lt;/h2&gt;

&lt;p&gt;I found out from a human. That is the part that still annoys me.&lt;/p&gt;

&lt;p&gt;In July 2026, mails I composed through AppleScript rendered completely blank for Outlook recipients. No error on my side. Mail did not consider this an error. Mail considered this Tuesday. The Sent copy looked fine, and I spent a while scrolling my own Sent folder insisting the words were right there. They were, on my side. The recipients got nothing, and it had already cost me by the time anyone said so.&lt;/p&gt;

&lt;p&gt;The lesson: looking at your own side of a send proves nothing. Not the compose window, not the API return value. Only the stored artifact counts. So the server treats every send as unverified until it has read the actual Sent copy back and confirmed the body text survived the trip. Bulk operations follow plan, review, apply. The assistant proposes, you approve, then it acts. And there is a read-only mode for anyone who wants an assistant nowhere near their outbox, which after that incident I consider a reasonable position.&lt;/p&gt;

&lt;p&gt;If you automate email and you do not verify sends, you are trusting a pipeline that has already failed silently for me once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it lives
&lt;/h2&gt;

&lt;p&gt;All of this ships as an MCP server, so any local MCP client can drive it. Claude Code, Claude Desktop, Cursor, VS Code. 21 tools, Python, MIT, 849 tests, entirely local. The wire contract has been additive only since v1.0.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/parasxos/apple-mail-mcp" rel="noopener noreferrer"&gt;https://github.com/parasxos/apple-mail-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install: &lt;code&gt;uvx apple-mailbox-mcp&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you have a bigger or weirder mail store than mine, I would genuinely like to hear what breaks first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correction, 27 Aug
&lt;/h2&gt;

&lt;p&gt;Michael Tsai wrote in with two sharp corrections to the example above, which I am happy to own. First, &lt;code&gt;message id&lt;/code&gt; in AppleScript is the RFC Message-ID header, and that header is not stored in the Envelope Index at all, so the whose clause in the tutorial pattern can end up parsing individual message files. It is pathological twice over. Second, the ROWID corresponds to the &lt;code&gt;id&lt;/code&gt; property on message objects, and due to a dictionary bug textual AppleScript cannot address it as &lt;code&gt;message id 12345&lt;/code&gt; without the raw chevron syntax. The server sidesteps this by speaking Apple events directly. The performance numbers and the core correspondence stand.&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>macos</category>
      <category>applescript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
