- Go 100%
Elke tweede release mislukte, en dat lag niet aan de release-stap. In de pool zitten runners van meer dan één architectuur: de mislukte runs duurden 190-210 seconden, de geslaagde 90-95. Op de trage — een ARM-machine — viel de stap om die de zojuist gebouwde linux-amd64 binary uitvoert om AI.md en openapi.yaml te schrijven. Een artefact dat je cross-compileert kun je niet zomaar draaien. `go run` compileert voor de machine waar de job toevallig op landt, dus dat is weg. De versie wordt nog steeds ingestempeld, dus de spec bij een release blijft het versienummer van die release noemen. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ShjhDbk1XUdJ3pMicrvRCf |
||
|---|---|---|
| .forgejo/workflows | ||
| cmd/aimail | ||
| internal | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
aimail
Je mail op je eigen machine, doorzoekbaar door een AI.
Haalt je mailbox binnen via IMAP — álle mappen, dus ook Verzonden en je archief — zet alles in één SQLite-bestand met een full-text index, en biedt dat aan via een lokale HTTP-API — zodat Claude (of wat je ook gebruikt) door je mail kan spitten en er namens jou eentje kan versturen.
Eén aimail.exe, geen installatie, geen runtime, geen cloud.
Snel starten
aimail sync # haal je mail binnen
aimail serve # zet de API aan
Alles staat in de map waar aimail.exe staat: de config, de database, klaar.
Zet hem dus ergens waar dat mag — D:\aimail\ bijvoorbeeld, niet in Program
Files. Kun je daar niet schrijven, dan wijkt hij uit naar %APPDATA%\aimail.
aimail init # een config om in te vullen
aimail init --proton # idem, maar met de instellingen van Proton Bridge erin
aimail init --print # naar het scherm, om zelf ergens neer te zetten
{
"accounts": [{
"name": "main",
"imap": { "host": "imap.jouwprovider.nl", "port": 993, "tls": true,
"username": "jij@domein.nl", "password": "…" },
"smtp": { "host": "smtp.jouwprovider.nl", "port": 465, "tls": true,
"username": "jij@domein.nl", "password": "…",
"from": "jij@domein.nl", "from_name": "Jouw Naam" }
}],
"api": { "addr": "127.0.0.1:8765", "token": "…automatisch gegenereerd…" },
// Optioneel. Zonder deze regel komt mail.db naast de config te staan.
// Schuine strepen werken ook op Windows, dus geen dubbele backslashes.
"database": "D:/aimail/mail.db"
}
Proton Mail (via Bridge)
Bridge draait op je eigen machine en biedt daar IMAP en SMTP aan met STARTTLS en een zelfondertekend certificaat:
"imap": { "host": "127.0.0.1", "port": 1143, "tls": false, "insecure": true,
"username": "jij@proton.me", "password": "<wachtwoord uit Bridge>" },
"smtp": { "host": "127.0.0.1", "port": 1025, "tls": false, "insecure": true,
"username": "jij@proton.me", "password": "<wachtwoord uit Bridge>",
"from": "jij@proton.me" }
"tls": false betekent hier STARTTLS, niet onversleuteld — dat is wat Bridge
aanbiedt. "insecure": true is nodig omdat Bridge zijn eigen certificaat tekent;
dat kan hier, want het verkeer komt niet van je machine af. Het wachtwoord is dat
uit het Bridge-venster, niet je Proton-wachtwoord. Staan 1143 en 1025 al vol, dan
kiest Bridge andere poorten en laat hij ze zien.
Commando's
aimail sync [--max N] [--folder F] |
nieuwe mail ophalen. Kan zonder zorgen herhaald worden |
aimail serve |
de lokale API draaien |
aimail search "factuur" [--since 2025-07-01] [--until 2025-07-31] |
zoeken vanaf de opdrachtregel |
aimail list [--limit N] [--folder F] |
de nieuwste berichten |
aimail show ID [--raw] |
één bericht: koppen, tekst, bijlagenlijst |
aimail attach ID [--out DIR] |
de bijlagen opsommen of wegschrijven |
aimail send --to a@b.nl --subject S --body B |
mail versturen |
aimail status |
hoeveel mail er staat, waar, en hoe ver een lopende sync is |
aimail init [--proton] |
een voorbeeldconfig neerzetten |
aimail docs [--api] |
de handleiding voor een AI, of de API-specificatie |
aimail measure |
wat er op de server staat, naast wat je gearchiveerd hebt |
Lezen vanaf de opdrachtregel
Alles wat de API kan, kan ook zonder de API:
aimail search "contract" --limit 5 # het nummer opzoeken
aimail show 4821 # koppen, tekst, wat eraan hangt
aimail show 4821 --raw > bericht.eml # het origineel, byte voor byte
aimail attach 4821 # wat zit eraan?
aimail attach 4821 --out . # zet het op schijf
aimail attach 4821 --index 2 --out D:\facturen
De bijlagen staan niet apart opgeslagen: ze zitten in het originele bericht, dat heel bewaard blijft. Ze worden er dus uitgehaald zoals ze binnenkwamen, en niets staat dubbel.
Bij het wegschrijven wordt de bestandsnaam ontdaan van zijn scherpe randjes.
Die naam komt namelijk uit de mail, en dus van de afzender: ..\..\Opstarten\ kwaad.exe is een naam die een bericht gewoon mag dragen. Alleen het laatste
stuk blijft over, rare tekens gaan eruit, en een bestand dat er al stond wordt
nooit overschreven — er komt een -1 achter.
Alles als JSON
Achter elk commando mag --json:
aimail status --json
aimail search "factuur" --limit 5 --json
aimail sync --json
aimail measure --json
Dan komt het antwoord als één JSON-document in plaats van als tekst voor een
mens — en dat geldt ook voor fouten, die als {"error": "…"} op stderr komen
met exitcode 1. Zo hoeft een script nooit halverwege over te schakelen op het
lezen van proza. Bij sync --json blijft de voortgangsregel weg (die zou dwars
door het document heen lopen); volg een lopende sync dan met
aimail status --json in een ander venster.
De API
Alles behalve /health wil het token uit je config, als
Authorization: Bearer <token> of als ?token=<token>.
GET |
/ |
documenteert zichzelf — handig als je een AI erop loslaat |
GET |
/stats |
hoeveel mail, over welke periode, en de voortgang van een lopende sync |
GET |
/search?q=&limit=&offset=&since=&until=&folder= |
full-text zoeken, beste match eerst |
GET |
/messages?limit=&offset= |
nieuwste berichten |
GET |
/messages/{id} |
één bericht met volledige tekst en bijlagenlijst |
GET |
/messages/{id}/raw |
het originele bericht |
POST |
/send |
versturen |
POST |
/sync |
nu ophalen. {"background":true} komt meteen terug en laat hem doorlopen |
GET |
/ai.md |
hoe een AI hiermee om moet gaan (geen token nodig) |
GET |
/openapi.yaml |
de volledige API-specificatie (geen token nodig) |
curl -s "http://127.0.0.1:8765/search?q=factuur&token=$TOKEN"
curl -s "http://127.0.0.1:8765/send?token=$TOKEN" \
-d '{"to":["jan@example.nl"],"subject":"Hoi","body":"Bericht"}'
Zoeken
De zoekopdracht is FTS5-syntax, dus meer dan losse woorden:
factuur losse term
"betalen voor 1 augustus" exacte zin
factuur AND juli beide
factuur NOT herinnering de een zonder de ander
reserv* begint met
subject:factuur alleen in het onderwerp
from_addr:leverancier.nl alleen van die afzender
folder:Verzonden alleen in die map
Punten en apenstaartjes zijn in FTS5 eigenlijk syntax; die worden automatisch
netjes aangehaald, zodat from_addr:bank.nl gewoon werkt. Accenten tellen niet
mee: reintegratie vindt ook reïntegratie.
Datums zijn geen tekst. In een mail uit juli 2025 staat het woord "juli
2025" niet, dus daar is een aparte parameter voor: --since 2025-07-01 --until 2025-07-31, of since=/until= op de API.
Klopt mijn archief?
aimail measure
Vraagt de server hoe groot elk bericht is — zonder er ook maar één te downloaden, want die maat staat al in zijn index — en zet dat naast wat jij hebt:
── on the server ── ──── archived ────
folder messages size messages size
INBOX 17090 1.2 GB 17090 1.2 GB
Sent 901 180 MB 901 180 MB
Trash 8412 410 MB 0 0 B (spam/trash, not archived)
All Mail 67915 3.6 GB 0 0 B (a second view of mail you already have)
Handig als je provider een heel ander getal noemt dan jij: klopt de kolom "server" met wat je verwacht, dan zit het verschil niet in je mail. (Bij Proton is de opslag één pot voor Mail, Drive, Pass en Calendar — dus meestal staan die gigabytes in Drive, en soms in de prullenbak dáárvan.)
Documentatie voor de AI
Naast deze README zitten er twee documenten ín het programma:
aimail docs # de handleiding zoals een AI hem moet lezen
aimail docs --api # de OpenAPI-specificatie
aimail docs --out . # allebei als bestand
Dezelfde twee hangen aan elke release, en de draaiende API serveert ze op
/ai.md en /openapi.yaml — zonder token, want moeten weten hoe je een token
meegeeft voordat je mag lezen hoe je een token meegeeft is een raadsel. Wijs een
AI dus gewoon op http://127.0.0.1:8765/ai.md en hij weet genoeg.
Een grote mailbox
Een mailbox van 25 GB wordt een database van ongeveer 28 GB: de originele
berichten worden bewaard zoals ze zijn (bijlagen incluis), en daar komt de tekst
plus de zoekindex bovenop. Zet "database" dus op een schijf waar dat past.
De eerste sync duurt uren, geen minuten — en de rem zit meestal niet bij aimail maar bij je mailserver. Dat is niet erg, want:
- Je kunt hem afbreken. Ctrl-C, afmelden, computer uit; wat binnen is blijft
binnen en de volgende
aimail syncgaat verder waar hij gebleven was. Een bericht wordt herkend aan map + UIDVALIDITY + UID, niet aan volgnummer. - Je kunt hem in stukken doen met
aimail sync --max 5000. - Je ziet waar hij is: één regel die zichzelf bijwerkt, met aantal, omvang, snelheid en een schatting van de resterende tijd.
- Ook vanuit een ander venster. De sync schrijft zijn voortgang elke paar
seconden in de database, dus
aimail statuselders vertelt hoe ver hij is — enGET /statsdoet hetzelfde voor een AI. Ligt hij eruit zonder netjes af te sluiten, dan zegtstatusdat ook in plaats van eeuwig "bezig" te melden.
$ aimail status
database D:/aimail/mail.db (12.4 GB)
messages 84.213
sync running: Archive, 84213 of ~176400 messages, 12.4 GB, nog ~2h14m
Hoe het in elkaar zit
cmd/aimail de opdrachtregel
internal/docs de handleiding en de API-spec, in het binary ingebakken
internal/imapclient IMAP: mappen opsommen en berichten ophalen
internal/mailmsg ontleden: gecodeerde koppen, tekensets, meerdelige berichten
internal/store SQLite + FTS5
internal/sync ophalen -> ontleden -> opslaan
internal/mailsend SMTP, inclusief het opstellen van het bericht
internal/api de lokale HTTP-API
Alles staat in één bestand (standaard %APPDATA%\aimail\mail.db, of waar
"database" naar wijst). Kopieer dat en je hebt je hele mailarchief mee. De
originele bytes zitten in een eigen tabel, niet in een kolom van de berichten:
bij tientallen gigabytes scheelt dat het verschil tussen een lijst die direct
verschijnt en een die eerst langs al je bijlagen moet. Het originele bericht wordt altijd bewaard, dus als
het ontleden ooit beter wordt, kan alles opnieuw geïndexeerd worden zonder nieuwe
download.
Dingen die je moet weten
Hij loopt al je mappen langs, behalve spam en prullenbak — daar wil je geen AI antwoorden uit halen — en behalve doorkijkjes: mappen die post laten zien die al ergens anders staat. Proton Bridge en Gmail bieden namelijk elk label én een "All Mail" aan als gewone IMAP-map, dus een bericht in je inbox met twee labels staat er vier keer in, met vier verschillende UID's. Die worden overgeslagen, want een bericht staat in precies één échte map.
Wil je het zelf bepalen, zet dan "folders": ["INBOX", "Sent", "Labels/Werk"] in
het imap-blok, of draai aimail sync --folder Verzonden. Wat je bij naam noemt
krijg je, ook een label — dan heb je erom gevraagd.
Er wordt niets op de server veranderd. Mappen gaan alleen-lezen open en de
berichten worden met PEEK opgehaald, dus je ongelezen mail blijft ongelezen en
er wordt nooit iets verwijderd.
Een tweede sync haalt niets opnieuw op. Een bericht wordt herkend aan map + UIDVALIDITY + UID; dat laatste getal is precies zolang geldig als de server zijn mailbox niet hernummert, en daarom horen die drie bij elkaar. Hernoem je een map in je mailprogramma, dan ziet aimail hem als nieuw en haalt hij hem opnieuw op.
Het wachtwoord staat leesbaar in de config. Dat is een bewuste keuze: het alternatief op Windows koppelt het bestand aan één gebruikersprofiel, waardoor "even naar mijn nieuwe laptop kopiëren" niet meer werkt. Het bestand krijgt wel rechten waarbij alleen jij erbij kunt.
De API luistert op localhost en wil een token. Hij kan mail versturen namens jou; dat hoort niet open op het netwerk te staan. Zet je hem toch op een ander adres, dan waarschuwt hij bij het starten.
Zelf bouwen
go test ./... # alles
GOOS=windows GOARCH=amd64 go build ./cmd/aimail # aimail.exe
Pure Go, ook de SQLite-driver — daarom cross-compileert het vanaf elke machine
naar één .exe zonder afhankelijkheden.