Presort: a Mac app that reads my mail with a model I run myself

Mail passes a model you run yourself and is split into calendar entries and reminders Mail Your model Calendar Reminders

Everything lands in iCloud Mail and then nothing happens to it. Apple gives you a mailbox and leaves the rest to you: the confirmation with a date buried in the third paragraph, the invoice due on the fourth, the parcel that has to go back before the eleventh. None of them announce themselves, and the ones I miss are never the ones I expected to miss.

A language model is genuinely good at this. It reads a page of Dutch marketing copy and finds the one sentence that commits you to something. The trouble is what every product offering this asks for first, which is a copy of your mailbox on their servers.

Your inbox is the most revealing thing you own. It has your bank in it, your doctor, your employer, your family, and a recoverable password for every account you have ever opened. Handing all of that to a company so that it can remind you about a dentist appointment is a poor trade, and it is a trade you cannot undo once it is made.

So Presort does the reading on a machine standing in my own house. Mail comes out of Apple Mail, goes to a model on hardware I own, and comes back. Nothing in between belongs to anyone else, and no part of the message is ever sent to a company. What comes out the other end lands in iCloud Calendar and Reminders, where the rest of my life already is.

The result is closer to what I wanted from iCloud Mail in the first place: a mailbox that notices things. It just runs on my own hardware rather than Apple’s, which turns out to be the only version of it I am willing to have.

Apple already does part of this

It is worth being straight about that. Data detectors have quietly pulled dates out of Mail for years, Apple Intelligence reads events out of mail and messages and offers them with the time and place already filled in, and since this spring there is a Calendar Inbox collecting what it finds. For an appointment with a date, a time and an address, that works well and I would not build a replacement for it.

The limit is in the shape of the thing it looks for. Apple’s detection is built around events: something happening at a moment, somewhere. Most of what I actually miss is not an event. Returning a parcel before the eleventh commits me to something without giving me anywhere to be. So does an invoice due on the fourth, or a subscription that renews unless I cancel before the twentieth. There is no meeting to add, and nothing gets detected.

A comparison table. A dentist appointment with a date, time and address is detected as a calendar event. A parcel return deadline, an invoice due date and a subscription renewal are not, though each needs a task with a reminder ahead of the date.
A deadline is a commitment with a date, not a place to be.

The second difference is who decides what counts. Apple’s list is Apple’s list, and it is the same on every Mac in the world. Here the categories are switches I can rewrite: invoices matter to me, parcels matter to me, and if travel bookings did not I could turn them off. That is a small thing until the day your inbox stops looking like everyone else’s.

The model gets no tools

This is the design decision everything else follows from. The model receives text and must answer with JSON in a fixed shape. It has no ability to call anything, write anything or reach anything. An instruction hidden inside an email can therefore produce a wrong form at worst, never an action.

The app then checks the answer rather than trusting it. Titles have to be between 2 and 120 characters. Dates have to fall between roughly a month back and a year ahead, because anything outside that is an invention. An appointment lasting more than 48 hours is refused. Whatever survives goes into a queue, and nothing is created until I press a button.

When it does create something, it writes into a calendar and a reminder list of its own, both named after the app and both sitting in iCloud alongside everything else. My existing calendars are read so that it can spot a clash, and left alone. If I want an entry in a real calendar I drag it across, and that act of dragging is how I accept it.

There is a switch to point it straight at a calendar I already use, with the reason not to sitting next to it: in a calendar of its own everything it filed is in one place, a mistake is easy to spot, and undoing one can never touch something I put there myself. The option is there because it is my calendar and not the app’s business to decide. I leave it off.

Or let it file by itself

Waiting for me to press a button is the default, not the only option. There is a switch that lets it file straight into the iCloud calendar and reminder list without asking, and once it is on, an appointment found at four in the morning is simply there when I wake up.

Only entries the model marked as high confidence go through that way. Anything less certain still waits for me, so turning the switch on narrows the pile rather than removing it. Everything still lands in the app’s own calendar rather than my real ones, every item can be undone from the list of what it has done, and the notification afterwards says what was filed rather than what is waiting.

It ships off, which is the right default for something that writes to your calendar unattended. I turned it on once I had watched it be right for a few weeks, and that order matters: trust after evidence, not before.

What it looks for is editable, what it returns is not

There are seven things it can watch for: appointments, tasks, invoices, subscriptions about to renew, parcels waiting at a pick-up point, travel bookings, and documents worth keeping. Each one is a switch, and behind each switch is a description you can rewrite in your own words.

What you cannot rewrite is the JSON schema wrapped around your description. That is deliberate, and it is the whole safety boundary: the checks above only work because the shape of the answer is fixed. So the app shows you the unchangeable parts in grey, above and below the box you are typing in, rather than hiding what it actually sends.

Two things it got wrong, and what they taught me

A return deadline came back as a reminder set for the deadline itself. Technically correct and completely useless, because a parcel that has to be at a drop-off point on the eleventh needs to leave the house before the eleventh. Deadlines now get a reminder a configurable number of days early, three by default, while keeping the real date as the due date.

The second was subtler. An Amazon return produced the task “print the return label”, which is a step in the job rather than the job. The instruction now says to name the main thing that has to happen and not a sub-step. Tested against the actual email, the same message now produces “send the wig back to Amazon”, which is a task I can act on without opening anything.

Both were found the same way: by running real emails through it and reading what came out, rather than by reasoning about what it probably did.

The card shows the result, not the find

Early on, each proposal showed a fragment of the email it came from. That turned out to be the wrong thing to look at, because what you are agreeing to is not the fragment but the calendar entry. Each card now shows the entry itself: the start and end time, the location, the note that will be written, which calendar it lands in, and when the reminder will actually go off. Everything you are about to say yes to, before you say it.

Three proposals waiting: a dental appointment going to the calendar, an energy bill going to the reminder list, and an attached statement going to Paperless-ngx. Each shows the fields that will be written.
Example data. Three shapes, three destinations, and in each case the fields that will actually be written.

The seventh thing is a file

The first six all end up in the same two places, as an entry in a calendar or a line in a list. The seventh does not. An invoice, a bank statement, an insurance policy, a ticket: those arrive as an attachment, and what I want with them is to keep them rather than to be reminded about them.

So they go to Paperless-ngx, the document archive already running here, and they go with what the mail says about them. Paperless works out a document’s title, sender and date by reading the scan, which costs the better part of a minute on my hardware. The mail had all three in its header before anything was scanned. The sender becomes the correspondent, the date printed on the invoice becomes the document date, and what the model read off the message makes a better title than OCR usually manages.

Tags are matched, never invented. The model is asked for a handful of plain words and those are compared against tags that already exist; anything without a match is dropped. My archive has 412 tags, and putting a list that long in front of a model is how you get one that stops choosing and starts completing the list instead. I had already learned that the expensive way, on a different tool.

The attachment is downloaded only once I approve it, never while the app is still scanning, and never under the name the sender chose. A filename is a path if you let it be one.

Paperless is the first of what the app calls a connection: a service outside this Mac that it can hand something to. Everything else Presort does happens on the machine, so anything that sends something elsewhere gets its own tab, its own switch, and stays off until you have filled it in. Its token lives in the Keychain rather than in the settings file, because that one opens every document I own.

Swift, because of the calendar

Electron would have been faster to build and cannot reach Calendar and Reminders without a native bridge, which is precisely the part the whole app exists for. So it is Swift, compiled with nothing but the command line tools, and the result is a three-megabyte application with no runtime to install.

The Presort app icon: three arrows sorting into separate lanes

It points at any endpoint that speaks the OpenAI shape, which for me means the machine described elsewhere on this site. For anyone else it would probably mean Ollama. Your mail is read on your own hardware by a model you chose, and that is the only claim about privacy the app needs to make.

The source is on GitHub under a noncommercial licence: use it, change it and fork it freely, but selling it is not covered.

Written with help

Most of this was built alongside Claude, in the ordinary sense of pair work: I decided what it should do and what it should refuse to do, and a great deal of the Swift was written by the model. That arrangement is worth describing honestly, because the interesting part is not that it wrote code. It is what the collaboration was actually like.

The best moments were the ones where something got measured instead of assumed. A delete button that appeared broken turned out to have a clickable area one point tall, which was found by asking the accessibility layer for its size rather than by staring at the code. The two failures above were found by pushing real emails through the model and comparing the output to the original.

And it needed correcting. A rewrite of an article on this site quietly acquired a price comparison I had never made and a claim that I had been running the setup for six months when it had been four weeks. Both were caught before publication. That is not an argument against working this way. It is an argument for reading what comes back, which is the same lesson the app itself is built around.

Leave a Reply

Your email address will not be published. Required fields are marked *