AoAH Day 17: OCaml JMAP to plaster my painful email papercuts / Dec 2025
After building a
Luckily, I've been self-hosting my

Why JMAP for email?
Historically, the protocol of choice for accessing email has been IMAP, and for years I used the Lwt ocaml-imap library that
The JSON Meta Application Protocol (or JMAP) was developed in response to this. The developers wrote:
JMAP is the result of efforts to address shortcomings [in existing protocols], providing a modern, efficient, easy-to-use API, built on many years of experience and field testing. -- JMAP, a modern open email protocol, Gondwana and Jenkins, May 2019
JMAP's exciting because it allows for the reuse of a number of other protocol implementations I've already built these past few weeks. I can use
Servers that support JMAP are still thin on the ground, but all my email endpoints do now support it. At work in the Cambridge Computer Lab, the enlightened sysadmins gave us Fastmail accounts as an alternative to having Exchange inflicted on us by the central University. Over at Recoil, my personal mail is switching to Stalwart JMAP. While there's still no good JMAP client that replaces my day-to-day email, all these services also expose IMAP for that purpose. This clears the way for me to vibecode up a JSON library in OCaml for the programmatic fragments that I so crave!
Building^H^H^Hvibing OCaml JMAP
Interestingly, I had also tried to build an OCaml JMAP waaay back when
But now that I've got a bunch of examples under my belt, the latest round of vibing of ocaml-jmap was extremely fast. The agent had examples of jsont use from
Building a browser-based JMAP client to test
However, I wanted to go a little further than just notebook style tutorials. In addition to compiling OCaml code to JavaScript, there's also really good support for programming browser API directly through libraries like Brr. These expose an OCaml interface that allows us to build up browser-specific logic to better integrate within it.

So I took the notebook idea one step further and prompted up an entire JMAP client that runs in the browser (albeit a very simple one!). This Json_brr library looks just like normal OCaml, except that it uses libraries designed to run in the browser as its dependencies. The interface shows some differences
type connection
val api_url : connection -> Jstr.t
(** [api_url conn] returns the API URL for requests. *)
(** {1 Session Establishment} *)
val get_session :
url:Jstr.t ->
token:Jstr.t ->
(connection, Jv.Error.t) result Fut.t
(** [get_session ~url ~token] establishes a JMAP session.
Instead of string, this uses Jstr.t for JavaScript strings. Instead of Eio, this also uses Fut.t to encode future promises.
But aside from those things, it's just plain OCaml! The implementation makes Websocket requests to connect to a remote JMAP server in the browser.
let fetch_json ~url ~meth ~headers ?body () =
Console.(log [str ">>> Request:"; str (Jstr.to_string meth); str (Jstr.to_string url)]);
(match body with
| Some b -> Console.(log [str ">>> Body:"; b])
| None -> Console.(log [str ">>> No body"]));
let init = Brr_io.Fetch.Request.init
~method':meth
~headers
?body
()
in
let req = Brr_io.Fetch.Request.v ~init url in
let* response = Brr_io.Fetch.request req in
To use jmap.html, I obtained a (read only!!) API key from my live email server, and then managed to connect to my live email and get a protocol debugger, all from my browser.

One important performance point about how this works is that some of the base libraries I'm using take advantage of OCaml's modularity in order to expose browser specific backends. For example, Jsont_brr uses the native browser JSON parser while still working with Jsont codecs. This sort of casual modularity is an extremely nice and undersung feature of OCaml, and was also key in
Fixing my notifications with programmable email
So now we have a working library, I wanted to go beyond the conventional email clients. With agentic coding, I should be able to construct hundreds of domain-specific bits of code that help me manipulate my email the way I want it done. This was the dream of #selfhosting and open source in the first place, but it was too much work to write all that glue code... until now.

One papercut that bugs both me and
JMAP keywords and CLI fragments
I built a domain-specific CLI called jmapq which exists solely to implement specialist workflows. I'll break this out into a private repo for my own use later, this is just a prototype!
Zulip sends a nicely structured email with a subject like "#Blogs > My 2025 Advent of Agentic Humps: a new library daily [Cambridge Energy & Environment Group]" every so often which I instructed the agent to parse into an OCaml data structure:
(** Parsed information from a Zulip notification email subject.
Subject format: "#Channel > topic [Server Name]" *)
module Zulip_message = struct
type t = {
id : string;
date : Ptime.t;
thread_id : string;
channel : string;
topic : string;
server : string;
is_read : bool;
labels : string list;
}
(** Parse a Zulip subject line of the form "#Channel > topic [Server Name]" *)
let parse_subject subject =
(* Pattern: #<channel> > <topic> [<server>] *)
let channel_re = Re.Pcre.regexp {|^#(.+?)\s*>\s*(.+?)\s*\[(.+?)\]$|} in
match Re.exec_opt channel_re subject with
| Some groups ->
let channel = Re.Group.get groups 1 in
let topic = Re.Group.get groups 2 in
let server = Re.Group.get groups 3 in
Some (channel, topic, server)
| None -> None
Once we have this, I then added another CLI command to list all the Zulip notifications in my email, which it outputs using nicely structured JSON output that is parseable by jq.

Then I instructed the CLI to give me more grouped views, so I can see notifications in my email by topic and by server (I now also get notifications from other Zulips I'm on like the OCaml or Lean).

And then the really sketchy bit. I swapped API keys to have a read/write one (it could delete all my email in theory), and then -- while only mildly sweating -- ran the timeout command. I did add a dry-run mode so I could see the query first before I let it rip.

And et voila, it added the right keywords and marked things as unread, and my live email view in Fastmail now allows for keyword searches that show me exactly what I want.

Reflections
Today's been great; I managed to find a papercut that's been bugging me for
years, and code up something quickly that genuinely makes my personal workflows
a little bit better. I used to do this a
I feel back in control again; although ocaml-jmap should be used by anyone else with extreme caution, I'm rigging up some ZFS snapshots of my own email so I can code up a few hundred custom agents over things and see how it goes. But mostly, I'm happy to have found a glimmer of the Resonant Computing manifesto through the medium of strong OCaml types, self hosted services and agentic coding glue:
Dedicated: Software should work exclusively for you, ensuring contextual integrity where data use aligns with your expectations. You must be able to trust there are no hidden agendas or conflicting interests. -- Resonant Computing Manifesto, 2025
I'm going to work on wrestling GitHub and Netdata under control next. And what about the other side of the Zulip notifications? Well, I'll knock up a Zulip bot in OCaml tomorrow, so stay tuned for that!