Alles komt binnen in iCloud Mail en daarna gebeurt er niets mee. Apple geeft je een mailbox en laat de rest aan jou over: de bevestiging met een datum die in de derde alinea verstopt zit, de factuur die op de vierde betaald moet zijn, het pakket dat voor de elfde terug moet. Geen van die mails meldt zich vanzelf, en de mails die ik mis zijn nooit de mails waarvan ik verwachtte dat ik ze zou missen.
Een taalmodel is hier echt goed in. Het leest een pagina Nederlandse marketingtekst en vindt de ene zin waarin je je ergens aan verbindt. Het probleem is wat elk product dat dit aanbiedt als eerste vraagt: een kopie van je mailbox op hun servers.
Je inbox is het meest onthullende wat je hebt. Je bank zit erin, je huisarts, je werkgever, je familie, en een wachtwoordherstel voor elk account dat je ooit hebt aangemaakt. Dat allemaal aan een bedrijf geven zodat het je aan een tandartsafspraak kan herinneren, is een slechte ruil, en het is een ruil die je niet meer terugdraait als hij eenmaal gemaakt is.
Dus doet Presort het lezen op een machine die bij mij thuis staat. Mail komt uit Apple Mail, gaat naar een model op hardware van mezelf, en komt weer terug. Niets daartussen is van iemand anders, en geen enkel deel van een bericht wordt ooit naar een bedrijf gestuurd. Wat er aan de andere kant uitkomt, belandt in iCloud Agenda en Herinneringen, waar de rest van mijn leven al staat.
Het resultaat komt dichter bij wat ik eigenlijk al van iCloud Mail wilde: een mailbox die dingen opmerkt. Alleen draait het op mijn eigen hardware in plaats van die van Apple, en dat blijkt de enige versie te zijn die ik wil hebben.
Apple doet hier al een deel van
Dat is eerlijk om te zeggen. Gegevensdetectie haalt al jaren ongemerkt datums uit Mail, Apple Intelligence leest afspraken uit mail en berichten en biedt ze aan met tijd en plaats al ingevuld, en sinds dit voorjaar is er een Agenda-inbox die verzamelt wat hij vindt. Voor een afspraak met een datum, een tijd en een adres werkt dat goed, en daar zou ik geen vervanging voor bouwen.
De grens zit in de vorm van wat er gezocht wordt. De detectie van Apple is gebouwd rond afspraken: iets wat op een bepaald moment ergens gebeurt. Het meeste wat ik echt mis, is geen afspraak. Een pakket voor de elfde terugsturen verplicht me tot iets zonder dat ik ergens hoef te zijn. Net als een factuur die op de vierde betaald moet zijn, of een abonnement dat verlengd wordt tenzij ik voor de twintigste opzeg. Er is geen afspraak om toe te voegen, en er wordt niets gedetecteerd.

Het tweede verschil is wie bepaalt wat telt. De lijst van Apple is de lijst van Apple, en die is op elke Mac ter wereld hetzelfde. Hier zijn de categorieën schakelaars die ik kan herschrijven: facturen zijn belangrijk voor mij, pakketten zijn belangrijk voor mij, en als reisboekingen dat niet waren, kon ik ze uitzetten. Dat is iets kleins, tot de dag dat je inbox er niet meer uitziet als die van iedereen.
Het model krijgt geen tools
Dit is de ontwerpkeuze waar al het andere uit volgt. Het model krijgt tekst en moet antwoorden met JSON in een vaste vorm. Het kan niets aanroepen, niets schrijven en nergens bij. Een instructie die in een e-mail verstopt zit, kan daardoor hooguit een verkeerd ingevuld formulier opleveren, nooit een actie.
De app controleert het antwoord daarna in plaats van erop te vertrouwen. Titels moeten tussen de 2 en 120 tekens lang zijn. Datums moeten tussen ongeveer een maand terug en een jaar vooruit liggen, want alles daarbuiten is verzonnen. Een afspraak die langer dan 48 uur duurt, wordt geweigerd. Wat overblijft gaat in een wachtrij, en er wordt niets aangemaakt tot ik op een knop druk.
Als hij iets aanmaakt, schrijft hij in een eigen agenda en een eigen herinneringenlijst, allebei vernoemd naar de app en allebei in iCloud naast al het andere. Mijn bestaande agenda’s worden gelezen zodat hij een overlap kan zien, en verder met rust gelaten. Wil ik een item in een echte agenda, dan sleep ik het erheen, en dat slepen is hoe ik het accepteer.
Er is een schakelaar om hem rechtstreeks op een agenda te richten die ik al gebruik, met de reden om dat niet te doen ernaast: in een eigen agenda staat alles wat hij heeft ingeplant op één plek, valt een fout makkelijk op, en kan het terugdraaien ervan nooit iets raken wat ik er zelf in heb gezet. De optie is er omdat het mijn agenda is, en het niet aan de app is om dat te bepalen. Ik laat hem uit.
Of laat hem het zelf inplannen
Wachten tot ik op een knop druk is de standaard, niet de enige optie. Er is een schakelaar waarmee hij zonder te vragen direct in de iCloud-agenda en de herinneringenlijst zet, en als die aanstaat, staat een afspraak die om vier uur ’s nachts gevonden is er gewoon als ik wakker word.
Alleen items die het model als zeer zeker heeft gemarkeerd, gaan op die manier door. Alles wat minder zeker is, wacht nog steeds op mij, dus met de schakelaar aan wordt de stapel kleiner, maar verdwijnt hij niet. Alles komt nog steeds in de eigen agenda van de app terecht en niet in mijn echte agenda’s, elk item kun je terugdraaien vanuit de lijst van wat hij heeft gedaan, en de melding achteraf zegt wat er is ingepland in plaats van wat er wacht.
Standaard staat het uit, en dat is de juiste standaard voor iets wat zonder toezicht in je agenda schrijft. Ik zette het aan nadat ik een paar weken had gezien dat hij het goed deed, en die volgorde doet ertoe: vertrouwen na bewijs, niet ervoor.
Wat hij zoekt kun je aanpassen, wat hij teruggeeft niet
Er zijn acht dingen waar hij op kan letten: afspraken, taken, facturen, abonnementen die bijna verlengd worden, pakketten die bij een afhaalpunt liggen, pakketten die nog onderweg zijn, reisboekingen, en documenten die het bewaren waard zijn. Elk daarvan is een schakelaar, en achter elke schakelaar zit een beschrijving die je in je eigen woorden kunt herschrijven.
Wat je niet kunt herschrijven, is het JSON-schema dat om je beschrijving heen zit. Dat is met opzet, en het is de hele veiligheidsgrens: de controles hierboven werken alleen omdat de vorm van het antwoord vastligt. Daarom laat de app de onveranderlijke delen in grijs zien, boven en onder het vak waarin je typt, in plaats van te verbergen wat hij werkelijk verstuurt.
Twee dingen die hij fout deed, en wat ik ervan leerde
Een retourtermijn kwam terug als herinnering op de dag van de deadline zelf. Technisch correct en volkomen nutteloos, want een pakket dat op de elfde bij een inleverpunt moet zijn, moet voor de elfde het huis uit. Deadlines krijgen nu een instelbaar aantal dagen van tevoren een herinnering, standaard drie, terwijl de echte datum de vervaldatum blijft.
De tweede was subtieler. Een retour bij Amazon leverde de taak “retourlabel printen” op, en dat is een stap in de klus, niet de klus zelf. De instructie zegt nu dat hij het belangrijkste moet noemen dat er moet gebeuren, en geen tussenstap. Getest met de echte e-mail levert hetzelfde bericht nu “de pruik terugsturen naar Amazon” op, en dat is een taak waar ik iets mee kan zonder iets te openen.
Allebei zijn ze op dezelfde manier gevonden: door echte e-mails erdoorheen te halen en te lezen wat eruit kwam, in plaats van te beredeneren wat hij waarschijnlijk deed.
De kaart toont het resultaat, niet de vondst
In het begin liet elk voorstel een fragment zien van de e-mail waar het vandaan kwam. Dat bleek het verkeerde om naar te kijken, want waar je mee instemt is niet het fragment, maar het agenda-item. Elke kaart laat nu het item zelf zien: de begin- en eindtijd, de locatie, de notitie die wordt weggeschreven, in welke agenda het terechtkomt, en wanneer de herinnering echt afgaat. Alles waar je ja tegen gaat zeggen, voordat je het zegt.

Het zevende is een bestand
De eerste zes komen allemaal op dezelfde twee plekken terecht, als een item in een agenda of een regel in een lijst. De zevende niet. Een factuur, een bankafschrift, een verzekeringspolis, een ticket: die komen binnen als bijlage, en wat ik ermee wil, is ze bewaren in plaats van eraan herinnerd worden.
Dus gaan ze naar Paperless-ngx, het documentarchief dat hier al draait, en ze gaan mee met wat de mail erover zegt. Paperless bepaalt de titel, afzender en datum van een document door de scan te lezen, en dat kost op mijn hardware het grootste deel van een minuut. De mail had alle drie al in de header voordat er iets gescand was. De afzender wordt de correspondent, de datum die op de factuur staat wordt de documentdatum, en wat het model uit het bericht haalde, levert een betere titel op dan OCR meestal voor elkaar krijgt.
Tags worden gekoppeld, nooit verzonnen. Het model wordt gevraagd om een handvol gewone woorden, en die worden vergeleken met tags die al bestaan; alles zonder match valt af. Mijn archief heeft 412 tags, en een lijst die zo lang is aan een model voorleggen is de manier om een model te krijgen dat stopt met kiezen en in plaats daarvan de lijst gaat aanvullen. Dat had ik al op de dure manier geleerd, met een andere tool.
De bijlage wordt pas gedownload als ik hem goedkeur, nooit terwijl de app nog aan het scannen is, en nooit onder de naam die de afzender koos. Een bestandsnaam is een pad als je hem dat laat zijn.
Paperless is de eerste van wat de app een koppeling noemt: een dienst buiten deze Mac waar hij iets aan kan overdragen. Al het andere wat Presort doet, gebeurt op de machine zelf, dus alles wat iets naar elders stuurt krijgt een eigen tabblad en een eigen schakelaar, en blijft uit tot je het hebt ingevuld. Het token staat in de Sleutelhanger en niet in het instellingenbestand, want dat token opent elk document dat ik heb.
Pakketten onderweg
Een achtste punt zoekt naar verzendberichten: een winkel die laat weten dat er iets is verstuurd, of een vervoerder die zegt dat het onderweg is. Daaruit haalt hij het trackingnummer, de vervoerder, wat er komt en de dag waarop het verwacht wordt, en het trackingnummer moet in de mail zelf staan voordat er iets wordt voorgesteld. De winkel en de vervoerder schrijven allebei over hetzelfde pakket, dus een nummer dat al in de wachtrij staat, wordt niet nog een keer voorgesteld.
Goedgekeurde pakketten gaan naar Parcel, de pakkettracker die ik al gebruik, en dat maakt het de tweede koppeling na Paperless. De API is streng: twintig toevoegingen per dag, de geweigerde meegeteld, en alleen voor Premium-abonnees. Dus controleert de app alles wat hij kan voordat hij er een verbruikt. De vervoerder moet overeenkomen met een vervoerder in de eigen lijst van Parcel. Een mail die alleen DHL zegt, wordt DHL Netherlands, omdat de Mac op dat land is ingesteld, en een mail die DPD zegt, wat bij meerdere past, krijgt een keuzelijst op de kaart. De postcode gaat alleen naar vervoerders die zonder postcode niet kunnen volgen, zoals PostNL, en een nummer dat Parcel al volgt, wordt overgeslagen. De eerste vijftien per dag gaan er vanzelf in als er niets aan is waar ik naar moet kijken; de rest wacht op de knop. Parcel heeft geen aanroep om een levering te verwijderen, dus dit is het enige wat de app inplant dat hij niet kan terugdraaien.
Het pakket wordt ook een herinnering op de dag dat het verwacht wordt, met het trackingnummer, de vervoerder en de afzender in de notitie. Veel verzendberichten noemen nooit een dag, en daarvoor vraagt de app hoogstens één keer per uur aan Parcel of de vervoerder er al een heeft genoemd. Als de vervoerder de dag verschuift, schuift de herinnering mee.
Met de verzendberichten in mijn eigen inbox, door hetzelfde model gehaald, kwamen alle vier eruit met een trackingnummer dat in de mail staat en een vervoerder die Parcel kent, en vier gewone berichten werden met rust gelaten. Amazon was de enige verrassing. Wat Amazon een trackingnummer noemt, is het bestelnummer, inclusief streepjes, en zo slaat Parcel het ook op. Dus gaan nummers nu met hun streepjes de deur uit en verliezen ze die alleen als er twee met elkaar worden vergeleken. De Parcel-kant heb ik nog niet lang genoeg gedraaid om te zeggen hoe die het over weken volhoudt.
Een model dat nadenkt tot er geen ruimte meer is
Die test had een omweg. De eerste run leverde helemaal niets op, voor de verzendberichten en voor de gewone berichten. De route waar mijn model achter draait, denkt na voordat hij antwoordt, en met de 400 tokens die de app per vraag toestaat, besteedde hij ze allemaal aan redeneren en stopte hij voordat hij een woord had geschreven. De app behandelde elk leeg antwoord als een bericht om de volgende keer opnieuw te proberen, en zei verder niets. Terugkijkend in de wachtrij bleek hij sinds eind augustus niets meer te hebben gevonden.
Presort controleert nu het model zelf. Hij vraagt naar één verzonnen afspraakbevestiging, met hetzelfde tokenbudget als het echte werk, en leest het antwoord zoals hij een echt antwoord leest. De controle draait als de app start, als het model verandert en na een run waarin geen enkel antwoord bruikbaar was. Als een model faalt, zegt hij waarom: het was niet bereikbaar, het antwoordde zonder de JSON, het had een makkelijk voorbeeld fout, of het dacht na tot er geen ruimte meer was om te antwoorden. In dat laatste geval vraagt hij het nog één keer met chat_template_kwargs ingesteld om het nadenken uit te zetten, een veld dat llama.cpp en vLLM lezen en dat LiteLLM doorgeeft. Is dat antwoord goed, dan blijft nadenken voor dat model uit. reasoning_effort op none zetten maakte geen verschil.
Een falend model verschijnt als een melding boven de wachtrij met een knop naar de instellingen, en als een notificatie als de run op de achtergrond gebeurde. In de instellingen kun je ook elk model testen dat het endpoint aanbiedt, waarbij embedding- en spraakmodellen worden overgeslagen. Bij mij werkten vier van de vijf lokale modellen alleen met nadenken uitgeschakeld en één werkte zoals hij was, en ze antwoordden allemaal in iets meer dan een seconde. De gehoste modellen liet ik buiten die run, omdat elke vraag aan ze geld kost, en dat staat in de instellingen ook naast de knop.
Swift, vanwege de agenda
Met Electron was het sneller gebouwd geweest, maar dat kan zonder native brug niet bij Agenda en Herinneringen, en dat is precies het deel waarvoor de hele app bestaat. Dus is het Swift, gecompileerd met niets anders dan de command line tools, en het resultaat is een app van drie megabyte zonder runtime die je moet installeren.

Je kunt hem richten op elk endpoint dat de OpenAI-vorm spreekt, en voor mij is dat de machine die elders op deze site beschreven staat. Voor iemand anders is dat waarschijnlijk Ollama. Je mail wordt op je eigen hardware gelezen door een model dat jij hebt gekozen, en dat is de enige bewering over privacy die de app hoeft te doen.
De broncode staat op GitHub onder een niet-commerciële licentie: je mag hem vrij gebruiken, aanpassen en forken, maar verkopen valt er niet onder.
Geschreven met hulp
Het meeste hiervan is gebouwd samen met Claude, in de gewone betekenis van samenwerken: ik besliste wat het moest doen en wat het moest weigeren, en een groot deel van de Swift is door het model geschreven. Die samenwerking is het waard om eerlijk te beschrijven, want het interessante is niet dat het code schreef. Het is hoe die samenwerking werkelijk was.
De beste momenten waren die waarin iets gemeten werd in plaats van aangenomen. Een verwijderknop die kapot leek, bleek een klikbaar gebied van één punt hoog te hebben, en dat vonden we door de toegankelijkheidslaag naar de afmetingen te vragen in plaats van naar de code te staren. De twee fouten hierboven zijn gevonden door echte e-mails door het model te halen en de uitkomst te vergelijken met het origineel.
En het moest gecorrigeerd worden. Een herschreven artikel op deze site kreeg ongemerkt een prijsvergelijking die ik nooit had gemaakt, en de bewering dat ik de opstelling al zes maanden draaide, terwijl dat vier weken was. Allebei werden ze voor publicatie opgemerkt. Dat is geen argument tegen deze manier van werken. Het is een argument om te lezen wat er terugkomt, en dat is dezelfde les waar de app zelf omheen gebouwd is.
