Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 1,637 words · 5 segments analyzed
Mon, Jun 8, 2026Hey there, me again! This is the second and last part in a duology around Thunderbird’s project to support Microsoft Exchange. About a month ago, I published a blog post that shared some of the project’s behind the scenes (as well some of my personal thoughts), in which I was focusing a lot on the high-level picture and how we defined a direction for the project. However, that article covers discussions that happened mostly around the start of the project, and didn’t get very technical. Although some of these conversations might have spanned over a longer period of time, the more practical, day-to-day progress of the project isn’t really mentioned - this is what this second part is for! We’ve already looked at where we were going, now let’s focus on how we’re getting there. Laying the foundations Our first task was to ask ourselves what code infrastructure we were missing, and although there was already some functionality available to us in C++, on the Rust side of things (where our protocol client was going to live) things kind of looked like the barest of wastelands. After all, this was the very first time we were going to write any Rust specifically for Thunderbird. We started by defining a baseline for what our protocol client needed to do. EWS traffic consists of of XML (SOAP) requests sent over an HTTP(S) connection, we therefore needed the ability to:
send out network traffic, and do so asynchronously build HTTP request and receive responses serialise request data into XML, and deserialise XML responses
In addition, we needed whatever solutions to those requirements to be low on boilerplate. EWS lists many operations and even more types, and by this point we didn’t have such a precise idea of how many of those we would need. Native asynchronicity Let’s start by looking at the first point on that list (or at least part of it): performing asynchronous operations. Like I mentioned in the first part of this series, asynchronous operations in Thunderbird rely heavily on callbacks functions to be called whenever a change occurred with an operation. On the other hand, Rust uses an async/await syntax somewhat similar to what also exists in JavaScript or Python, so we’ll need a way to link the two together.
A long-standing issue of compatibility between Rust and C++ has been the lack of a stable ABI on either side making it difficult to carry types more complex than what the C ABI supports over the language boundary. A few solutions exist to address this, cxx being a pretty well-known and generic one. In our case, however, we’ll use something called XPCOM (Cross-Platform Component Object Model), which is a custom Mozilla cross-language compatibility layer used specifically by Firefox and Thunderbird. XPCOM uses interfaces written in an Interface Description Language called XPIDL. Interfaces defined this way can be implemented in either JavaScript, C++ or Rust, and once registered with a contract ID (a unique human-readable string, e.g. @mozilla.org/my-component;1) each implementation can also be instantiated from any of these languages (note that XPIDL also defines attributes that can constrain which language can implement or use a given interface). In our case, the interface we’re interested in is called nsIChannel (fun fact: a number of interface names in both Firefox and Thunderbird start with ns, which stands for “Netscape”, the company the Mozilla project originated from). In Mozilla terminology, a channel represents a resource fetch, which can be performed both synchronously and asynchronously. This is interesting to us, because Necko, Firefox’s networking component which we also inherit, uses channels to perform HTTP requests. We could use an off-the-shelf HTTP crate like reqwest, but using Necko comes with a couple of main advantages:
requests and responses can be visualised in Thunderbird’s devtools (which is another thing we inherit from Firefox) requests sent through Necko adhere to users’ network and web content settings, such as cookie settings, without specific support needed on the Thunderbird side
When when a channel is “opened” asynchronously, it takes an nsIStreamListener as its listener. This is an interface with three methods (two of them inherited from nsIRequestObserver):
OnStartRequest, which notifies that the operation has started OnDataAvailable, which notifies that new data for the operation is available, and is called with a stream that contains this new data OnStopRequest, which notifies that the operation has stopped, and is called with a status code indicating whether the operation has succeeded or failed
On the Rust side of things, the async/await syntax is powered by the Future trait.
This trait defines an associated type Output, which is the type to which the future resolves (once the operation has completed), as well as a poll method that is called by the task scheduler to attempt to resolve the future into its Output type. This poll method runs synchronously, and returns a variant of the Poll enum; Poll::Pending if the operation has not completed yet, and Poll::Ready if the operation has successfully completed, with the result of type Self::Output wrapped inside it. The poll method might be called every now and then by the scheduler to check on the state of the operation, but futures also have the option to tell the scheduler that it has finished and should be checked back on using a Waker (which can be retrieved from the Context passed to every poll call). With this in mind, here’s what we did:
We defined a custom implementation of nsIStreamListener, called BufferingStreamListener, which concatenate all of the data retrieved through OnDataAvailable into its inner bytes buffer (Vec<u8>), and records the status code given to OnStopRequest. We defined the struct AsyncChannelOpener, which can be constructed from an nsIChannel, and implements the Future trait.
Once AsyncChannelOpener::poll is called for the first time, it instantiates a new BufferingStreamListener and calls nsIChannel::AsyncOpen on its inner channel with that listener. BufferingStreamListener is responsible for letting the scheduler know when to poll again, so it holds a Waker for the task, which AsyncChannelOpener updates each time poll is called. Once the request completes and the listener’s OnStopRequest is called, the Waker is used and the AsyncChannelOpener is polled again, at which point it returns a Result which contains either the failure code (if the request failed), or a tuple with the nsIChannel and the contents of the listener’s buffer. We include the channel in the return value because the consumer might need to use it to read extra information, such as the response’s status code for HTTP traffic. A quick aside: we initially built this code to be very generic and be possibly extended in the future with support for other asynchronous XPCOM operations beyond nsIChannel::AsyncOpen. In reality, the need for this never really materialised and this approach could only really support operations that take an nsIStreamListener anyway, so we recently took steps to simplify it.
Idiomatic web requests Now that we can turn an nsIChannel into a Rust-native Future that can be awaited, we need to figure out how to create this channel in the first place. This is done through an XPCOM service that implements the nsIIOService interface, which offers a NewChannel method. This method takes a URI and instantiates a new nsIChannel which implementation matches that URI’s protocol scheme (a component can be registered as the protocol handler that creates the channels for a given URI scheme in its registration, for example the one we use to create channels for EWS messages is registered here). This means that if we give that method an HTTP(S) URI, it will return a channel that sends HTTP(S) requests. With that in mind, let’s have a look at how we can send a simple HTTP GET request with Necko: use std::ptr;
use http::Method;
use nserror::nsresult; use nsstring::nsCString; use xpcom::{ get_service, interfaces::{ nsIContentPolicy, nsIHttpChannel, nsILoadInfo, nsIIOService, nsIPrincipal, nsIScriptSecurityManager, }, RefPtr, };
use xpcom_async_glue::AsyncChannelOpener;
async fn send_request(uri: String) -> Result<Vec<u8>, nsresult> { let iosrv = get_service::<nsIIOService>( c"@mozilla.org/network/io-service;1" ).ok_or(nserror::NS_ERROR_FAILURE)?;
let scriptsecmgt = get_service::<nsIScriptSecurityManager>( c"@mozilla.org/scriptsecuritymanager;1" ).ok_or(nserror::NS_ERROR_FAILURE)?;
// XPCOM methods return an `nserror::nsresult` to indicate // success or failure; the actual return value is an pointer // passed as an in/out parameter. `getter_addrefs` turns the // `nsresult` into a `Result` for us, and also ensures the // pointer is correctly integrated within the internal ref // counting system. let principal: RefPtr<nsIPrincipal> = getter_addrefs( |p| unsafe { scriptsecmgr.GetSystemPrincipal(p) } )?;
// Use a Mozilla-internal string type. It derefs into an // `nsACString`, which is the "abstract" type that is generally // expected by XPCOM methods. let uri = nsCString::from(uri);
let channel: RefPtr<nsIChannel> = getter_addrefs( |p| unsafe { iosrv.NewChannel( // We need to turn the URI into a // `*const nsACString`. &raw const *uri, ptr::null(), ptr::null(), ptr::null(), // Turns the `RefPtr<T>` into a // `*const T`. principal.coerce(), ptr::null(), nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, nsIContentPolicy::TYPE_OTHER, p ) } )?;
// Cast the channel into an `nsIHttpChannel`, which inherits // from `nsIChannel` and exposes methods specific to HTTP. // There's no guarantee that the underlying `nsIChannel` // implementation also implements `nsIHttpChannel`, so we // need to catch this as an error at runtime. let http_channel = channel .query_interface::<nsIHttpChannel>() .ok_or(nserror::NS_ERROR_FAILURE)?;
// Note: the request method defaults to GET, so in practice // we might not need this, but this works better for // illustrating. let method = nsCString::from("GET"); unsafe { http_channel.SetRequestMethod(&raw const *method) }.to_result()?;
let (_channel, bytes) = AsyncChannelOpener::from(channel).await?;
Ok(bytes) } If you’re like me, you’ll notice a few things right off the bat:
The multiple unsafe blocks, because every method from an XPCOM interface implementer might come from C++ or JavaScript and is therefore unsafe Pointers, pointers everywhere The error handling which leaves a lot to be desired That’s a bunch of code for a simple GET request, with no body, no headers… what happens when we want to start making more complex requests?
That last question is simple to answer: it gets more complicated.