async imap (especially) in rust
async imap clients
the rust ecosystem regarding imap roughly looks like this
- rust-imap: blocking
std::net::TcpConnection
sockets - tokio-imap: non-blocking using the tokio framework’s protocol framing mechanisms
- async-imap: fork of rust-imap replacing
std::net
withasync_std::net
when you say async in the current rust ecosystem, it’s interpreted to mean whether you’ve threaded the right async
and await
keywords through your code to make it use non-blocking I/O operations instead of blocking the whole thread.
another type of async
i want to talk about another aspect of asynchronicity here: the decoupling of requests and replies to and from the imap server. what all three libraries listed above have in common is that you cannot really have more than one operation in progress at the same time. the imap protocol would very much allow that for compatible (whatever that means) commands, and, more importantly, requires the client to be able to deal with so-called “unsolicited responses” from the server at practically any time anyway. unsolicited responses are status updates that the server sends when some mailbox contents or attributes have chanegd, but those changes were not initiated by the client (e.g. a new mail arrived, or another client simultaneously changed message flags).
an architecture vision
what i think might be a better design for such a client would be
- submitting a request to the server returns an opaque token with which the response can be awaited
- multiple requests can be sent at the same time, returning multiple tokens
this would (i think) require somewhat splitting up the client into a sender and a receiver end, where the sender sends data to the server and generates tokens, and the receiver continuously listens for any available data from the server.
this in turn generates some new complexities that i’m not yet sure of how to handle or if all of them are worth it (just thinking out loud here, basically):
- we need to continuously listen for read-readynes and sometimes also write on the socket, ideally without interrupting the read
- there will be no inherent mapping of which command triggered which response; if we decouple the sender and receiver too much, we might not know e.g. which imap folder a response belongs to (we can make the sender send the commands it submits to the receiver in some channel, but now we have a race-condition between the sender -> receiver channel and the imap server -> receiver socket).
i know (because i helped implement it) that at least rust-imap has some code sprinkled accross the various methods implementing the imap commands that basically match the type of response and if it’s not one this method expects, the response is put into an unsolicited_responses
channel, so library users can react to them separately (the imap server is free to send updates whenever, but at least some implementations send them as additional lines when they’re responding to a command anyway, which is how the imap CHECK
command came to be i think). i think this is suboptimal, because it still requires the library user to submit commands every once in a while (even if the server sends responses earlier, the rust-imap implementation doesn’t check for new server replies until you’ve sent a command).
async usage instead of async implementation
i’m not sure what the overhead or different hassle will be for going full async, e.g. having to re-connect data that the rust-imap approach already has connected. on the other hand i think it’s the only way to really capture what the imap protocol allows. consider the NOTIFY
extension (in my opinion a terribly-underrated extension), which will let a client tell the server to send unsolicited updates whenever something in a list of mailboxes changes. this was not previously possible, instead you had to open one connection per folder that you want to monitor, select that folder and enter the IDLE
state, in which the server will tell you if something happens in that folder, so you can leave the IDLE
state and check what it was that happened.
without having written a mail client yet, i think if i did, i’d want to basically get a stream of events from the imap server for any interaction with the server whatsoever. that would include “a new message arrived in folder foo” as well as “your search returned another result: bar” or “here’s the body of message XYZ you requested earlier”. the two requests that caused these three responses could be in-flight at the same time, and receiving mail messages doesn’t care what you’re currently doing via imap anyway.
so in conclusion, i think it is more important for the users of an imap client library to have the usage of the library support asynchronous requests and responses (“async usage”) than having the implementation done using the async/await I/O primitives (“async implementation”).
PS: async vs. non-blocking
what the async keyword in rust does for you is instead of blocking on an I/O operation, we check if any data is available and otherwise return some non-readynes indication. that in and of itself is not asynchronous in the sense that things could happen in parallel/interleaved. what makes it async as in parallel is that there is (in rust) a scheduler in the background that receives the non-readynes return value and says “ok, if this thing can’t do any I/O now, i’ll try the next thing in my queue”.