Browsing your mail with Rust and Qt

2018-09-16

Let’s write a mail viewer with Rust and Qt. This is another blog about Rust Qt Binding Generator, the project that lets you add a Qt GUI to your Rust code, or if you will, add Rust to your Qt program.

Rust Qt Binding Generator (Logo by Alessandro Longo)
Rust Qt Binding Generator (Logo by Alessandro Longo)

Previous blogs about Rust Qt Binding Generator covered the initial announcement, building a simple clock, and making a todo list. Now, I’ll show a bit of how to make a more complex application: an email reader.

But first: Rust Qt Binding Generator takes an unusual approach to bindings. It is not a complete binding from the Qt API to Rust because I think that that is impossible: the C++ Qt API does not have all the safety guarantees that Rust has. So the binding API would be full of unsafe negating a big advantage of using Rust.

Instead, Rust Qt Binding Generator generates a binding for just your code to make your Rust code available to a Qt GUI.

In the first tutorial, we showed a clock. The model shared between Rust and Qt had three simple properties: hour, minute and second. The second blog was about a todo application. There, the model was a list of todo items shared between Rust and Qt.

Getting the code

This time, we move on to a more complex object: a tree.

A mail viewer written with Rust and Qt
A mail viewer written with Rust and Qt

We’re keeping with the theme of personal information management and are writing an email viewer. It can read mail from MailDir folders and IMAP servers. It is completely readonly and will not even change the state of your messages from unread to read. I feel comfortable using it on my own mails alongside my real mail programs.

The code is available in my personal KDE space. It requires Rust, Cargo, Qt, CMake, OpenSSL, and ninja (or make). You can retrieve and compile it with

The code is about 2200 lines of Rust and 550 lines of QML. Parsing mails and communicating over IMAP is done by three crates: mailparse, imap-proto and imap.

The shared data model

In an email application there are usually two prominent trees: one shows the email folders and the other shows the messages in the selected folder.

First we model the list of folders. Here is the JSON object from bindings.json that does this.

The type of MailFolders is Tree. Each node in the tree has two properties: name and delimiter. Rust Qt Binding Generator generates Qt and Rust code from this. The Qt code (Bindings.cpp and Bindings.h) defines an implementation of QAbstractItemModel. This is the same base class as in the todo example. This time, it holds a tree instead of a list.

There is also Rust code generated. The file interface.rs is the binding to the Qt code. It defines a trait MailFoldersTrait that the developer needs to implement in a struct called MailFolders.

We’ll discuss some parts of the Rust implementation file.

The implementation should be backed by a structure. There are two structures: MailFolder which represents a node in the tree and MailFolders which contains all the nodes in a Vec and interfaces for communicating with other parts of the program.

In the tree, each node has a unique index. The index is used by Qt to find out information about the node, like how many children (rows) it has or to get out data like the name.

These functions correspond to the C++ virtual functions in QAbstractItemModel.

Doing the work in a thread

The user interface should stay responsive. So intense and slow work like reading and parsing email is done in a separate thread. The user interface starts a thread to do the hard work and sends commands to it via a channel.

When new data is available, the UI needs to update. This must be done by the UI thread. When the processing thread has new data it emits a signal to the UI thread. The UI thread then aquires accesses the data via a mutex that is shared between the two threads.

Communication between GUI and processing threads
Communication between GUI and processing threads

QML for the folders

The Rust-implemented model is used from the QML. The connection between the TreeView and the model is made by the line model: mailmodel.folders. Each node is rendered according to the Text delegate. When the user selects a different folder the model is notified of this by handling the onCurrentIndexChanged event.

TreeView {
    id: folders
    model: mailmodel.folders
    TableViewColumn {
        title: "Name"
        role: "name"
        width: folders.width - 20
        delegate: Text {
            text: icon(styleData.value) + " " + styleData.value
            verticalAlignment: Text.AlignVCenter
        }
    }
    onCurrentIndexChanged: {
        //...
        mailmodel.currentFolder = path;
    }
    style: TreeViewStyle {
        highlightedTextColor: palette.highlightedText
        backgroundColor: palette.base
        alternateBackgroundColor: palette.alternateBase
    }
    headerVisible: false
    frameVisible: false
}

Other parts

The list of folders is one of five object defined in the model. The others are the tree for the message threads in a folder (middle pane in the screenshot above), the current email (right pane), the list of attachments for the current email and an overall object that contains all the other ones. The latter, MailModel, is the initial entry point for the user interface. The user-initiated commands are sent to the processing thread from that overall object.

Trying it out

Create a configuration file for MailDir or IMAP.

The path for the MailDir configuration is the folder that contains .inbox.directory.

and run the code

Concluding

This GUI is built with QML via Qt Quick Controls. One might as well write one with Qt Quick Controls 2, QWidgets or Kirigami. The majority of the code, the Rust code, could stay the same. With the appropriate abstractions one might even use a different GUI framework and still keep the core application logic. Just imagine: KDE and GNOME joined together by Rust.

Comments

Post a comment