Email notifications come to Mollymawk
2026-01-26Mollymawk now supports sending email notifications to users 🎉.
In the world of self-hosted virtual machines, "set it and forget it" is often the goal. However, forgetting shouldn't mean being left in the dark. With this update, Mollymawk keeps you informed about your running unikernels.
How to send emails from a unikernel
For this to work, We utilized two key libraries from the Mirage ecosystem: Mr. MIME for crafting the content and Sendmail_Mirage which implements the SMTP protocol.
1. Constructing the Email with Mr. MIME
Email is deceptively complex. To ensure our messages render correctly across different clients, we use mrmime. It handles the heavy lifting of RFC compliance (like RFC2045 for MIME types and RFC5322 for headers).
Instead of concatenating strings manually, we describe the email structure using OCaml types. Here is a simplified example of how we construct the header and body:
let email =
let header =
let open Mrmime in
Header.of_list Field.[
Field (Field_name.subject, Unstructured, Unstructured.Craft.(compile (subject_of_strings "Update Available")));
Field (Field_name.v "From", Addresses, [ `Mailbox from_email ]);
Field (Field_name.v "To", Addresses, [ `Mailbox user_email ]);
Field (Field_name.content_type, Content, Content_type.(make `Text (Subtype.v `Text "html") Parameters.empty));
]
in
(* Now we construct the body of the email as an html string *)
let body =
let html_string =
Tyxml_html.(
html
(head
(title (txt "Unikernel updates available"))
[ meta ~a:[ a_charset "UTF-8" ] () ])
(body
[
div
[
paragraph
[
txt "Test email body";
];
];
]))
in
Format.asprintf "%a\n" (Tyxml_html.pp ()) html_string
in
(* We convert the body into a stream to feed it to Sendmail_mirage *)
let body_stream =
let sent = ref false in
fun () ->
if !sent then None
else (
sent := true;
Some (body, 0, String.length body))
in
(* We now combine the header with the body stream *)
let part = Mt.part body_stream in
Mt.make header Mt.simple part
2. Sending the email with Sendmail_mirage
Once the email is constructed, we need to talk to an SMTP server. For this, we use sendmail_mirage, a library that implements the SMTP protocol.
One of the challenges in a distributed environment is reliable networking. In our implementation, we use a "Happy Eyeballs" approach. This means Mollymawk attempts to resolve both IPv4 and IPv6 addresses for the mail server and connects to whichever responds fastest.
Here is a glimpse of how we submit the email using this module:
(* We set up the connection using Happy Eyeballs for fast IPv4/IPv6 resolution *)
let happy_eyeballs = HE.create ~getaddrinfo stack in
(* We stream the Mr. MIME content directly to the SMTP server *)
let streamer =
let s = Mrmime.Mt.to_stream email in
fun () -> Lwt.return (s ())
in
Mailer.submit
~domain
~destination:(`Ipaddrs [ server_ip ])
~port:587
happy_eyeballs
sender
recipients
(fun () -> streamer ())
By streaming the content, we keep memory usage low, even if we decide to send larger reports in the future.
We launched this update with one major notification:
Unikernel Updates
This was our motivation for adding email notifiations. In a fast-moving environment, keeping your unikernels up to date is critical for security and performance.
Mollymawk does a daily background check whether any updates for the running unikernels are available in our reproducible build infrastructure. If it finds updates, it notifies each user individually via email. This ensures you never miss a critical security patch or performance improvement.
This only works for unikernels which are deployed from our reproducible builds platform (builds.robur.coop).

We plan to support more types of email notifications in the future.
The work for this milestone was achieved in: