Using Akonadi in applications

Displaying and modifying data provided by Akonadi

This tutorial will guide you through the steps of creating an Akonadi application from scratch. This powerful framework allow us to easily display and manipulate personal information management (PIM) data. We will use it to create a simple QML and Kirigami application that will allow the user to view their emails.

Preparation

We can kick-start the application by using KAppTemplate, which can be found as "KDE template generator" in the development section of the application launcher menu, or by running kapptemplate in a terminal window.

First, we select the Kirigami Application in the QtGraphical section of the program. We can then give our project a name and continue through the following pages of the wizard to complete the template creation.

KAppTemplate with Kirigami Application selected

A look at the generated project's top level directory shows us the following files:

CMakeLists.txt
src/
org.example.quickmail.appdata.xml
org.example.quickmail.desktop

and the following files can be found in the sub directory src:

CMakeLists.txt
contents
main.cpp
resources.qrc

At this stage, it is already possible to compile the application, so we can already check if our development environment is set up correctly by creating the build directory and having CMake either generate Makefiles or import the project in KDevelop.

Generating Makefiles

Generating our makefile is as easy as entering the project's top-level directory...

mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=debugfull ..

...and running the build using make as usual.

Adjusting the project dependencies

First thing's first: we need to add the Akonadi dependencies to the CMakeLists.txt files in both the top-level directory and in the the src directory.

Add the following line to the file in the top-level directory:

set(LIBKDEPIM_VERSION "5.16.0")

# Find KdepimLibs Package
find_package(KF5Akonadi ${LIBKDEPIM_VERSION} CONFIG REQUIRED)
find_package(KF5Libkdepim ${LIBKDEPIM_VERSION} CONFIG REQUIRED)

and add a new target library in src/CMakeLists.txt

target_link_libraries(quickmail
    ...
    KF5::AkonadiCore
    KF5::AkonadiAgentBase
    KF5::AkonadiWidgets
    KF5::AkonadiXml
)

Creating the main class

The usual way to expose information to the QML engine is by creating QObjects and use then use these to expose Q_PROPERTIES. So let's create a simple QuickMail class inside src/quickmail.{h,cpp} and then add our new cpp file to the targets in src/CMakeLists.txt.

// src/quickmail.h

#pragma once
#include <QObject>

class QuickMail : public QObject
{
    Q_OBJECT

public:
    QuickMail(QObject *parent = nullptr) : QObject(parent) {}
};

We then make it available to the QML engine as a singleton.

// src/main.cpp
int main(int argc, char *argv[])
{
    ...

    QuickMail mail;
    qmlRegisterSingletonInstance("org.kde.quickmail.private", 1, 0, "QuickMail", &mail);
}

Initialization

Since the application will depend on Akonadi, we have to make sure this is running before our application uses it. We can make sure this is the case by starting Akonadi if it is not already running. This is handled by the Akonadi::Session and the Akonadi::ServerManager classes. A Session will asynchronously start the Akonadi connection and the ServerManager instance will allow us to query the state of the connection. Once the server is loaded, we will notify the UI that it has loaded by setting the loading property to false.

namespace Akonadi {
    class Session;
}

// src/quickmail.cpp
class QuickMail : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)

    ...
    bool loading() const;
    Akonadi::Session *session() const;

private:
    bool m_loading;
    Akonadi::Session *m_session;
};

In quickmail.cpp we need two new include directives:

#include <ServerManager>
#include <Session>

Then, in the constructor, we need to implement our Akonadi connection:

using namespace Akonadi;

QuickMail::QuickMail(QObject *parent)
    : QObject(parent)
    , m_loading(true)
{
    m_session = new Session(QByteArrayLiteral("KQuickMail Kernel"), this);

    // TODO initialization of the model

    // Loading state change handler
    if (Akonadi::ServerManager::isRunning()) {
        m_loading = false;
    } else {
        connect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged,
                this, [this](Akonadi::ServerManager::State state) {
                    if (state == Broken) {
                        qApp->exit(-1);
                        return;
                    }
                    bool loading = state != Akonadi::ServerManager::State::Running;
                    if (loading == m_loading) {
                        return;
                    }
                    m_loading = loading;
                    Q_EMIT loadingChanged();
                    disconnect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged, this, nullptr);
                });
    }
}

bool QuickMail::loading() const
{
    return m_loading;
}

Session *QuickMail::session() const
{
    return m_session;
}

If the application fails to start Akonadi, it simply quits. A real application should probably tell the user about that, though.

Setting Up the Mail Folder Navigation

To make them easier to use, mail apps often organize the emails in multiple folders: the inbox folder, the spam folder, the sent folder, etc. Here we set up code to allow us to display folders in the user interface.

CMake

Akonadi at its core, is a cache layer, and Akonadi and the application communicate using a special protocol. To make our lives easier as app devs, the Akonadi client library provides pre-built models to access the data stored in it without writing low-level protocol code.

In fact, there's a super-specialized set of models specifically designed for email available in a library: AkonadiMime, which connects Akonadi and KMime to provide models convenient for mail apps, like the one we are writing.

First, we need to add two new KDE PIM libraries in the CMakeLists.txt:

find_package(KF5AkonadiMime ${LIBKDEPIM_VERSION} CONFIG REQUIRED)
find_package(KF5Mime ${LIBKDEPIM_VERSION} CONFIG REQUIRED)

To properly link the additional libraries, add KF5::Mime and KF5::AkonadiMime to the source directory's src/CMakeLists.txt in the target_link_libraries call.

target_link_libraries(quickmail
    ...
    KF5::Mime
    KF5::AkonadiMime
)

C++ Folder model

In quickmail.h we need to add three Q_PROPERTY. loading will tell our QML view if the model was loaded and descendantsProxyModel contains the tree model of the mail folders.

#pragma once

class KDescendantsProxyModel;

....

class QuickMail : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
    Q_PROPERTY(KDescendantsProxyModel *descendantsProxyModel READ descendantsProxyModel CONSTANT)

public:
    ...
    KDescendantsProxyModel *descendantsProxyModel() const;

private:
    ...
    KDescendantsProxyModel *m_descendantsProxyModel;
};

With that, we can now extend the QuickMail constructor to also create the models.

The first thing we will is to create an Akonadi::Monitor. This is a crucial step in setting up the model. We are using it to determine which information we want to fetch and keep track of, and it will also emit signals when the collection is changed in any way. To keep the example simple, we are using a simple monitor that fetches all of the available information.

    ...
    m_session = new Session(QByteArrayLiteral("KQuickMail Kernel ETM"), this);

    auto monitor = new Monitor(this);
    monitor->setObjectName(QStringLiteral("CollectionMonitor"));
    monitor->fetchCollection(true);
    monitor->setAllMonitored(true);

The next step is to create the main data model using an Akonadi::EntityTreeModel. This contains all the items and collections inside Akonadi. However, since for this application we only want to displays emails, we will need to filter out data types we aren't interested in. We are looking for MIME messages, or in terms of MIME type, "message/rfc822" (the MIME type is conveniently provided by KMime::Message::mimeType()). This kind of filtering is conveniently supplied in the form of a proxy model called Akonadi::CollectionFilterProxyModel.

    auto treeModel = new Akonadi::EntityTreeModel(monitor, this);
    treeModel->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation);

    auto entityTreeModel = new Akonadi::CollectionFilterProxyModel();
    entityTreeModel->setSourceModel(treeModel);
    entityTreeModel->addMimeTypeFilter(KMime::Message::mimeType());

To use the tree model in a QML view, it needs to be converted to a list first. This is where KDescendantsProxyModel is handy. It's a proxy model that will turn a tree model into a list model while providing some information like indentation or the collapse status of the items. This makes it possible to implement a tree view in QML.

    // Proxy model for displaying the tree in a QML view.
    m_descendantsProxyModel = new KDescendantsProxyModel(this);
    m_descendantsProxyModel->setSourceModel(entityTreeModel);

    // Loading state change handler
    ...

Don't forget to register the KDescendantsProxyModel in the main.cpp file.

    qRegisterMetaType<KDescendantsProxyModel*>("KDescendantsProxyModel*");

The User Interface

In the QML file located at src/content/ui/main.qml, we remove the default mainPageComponent and add the following code instead:

    pageStack.initialPage: QuickMail.loading ? loadingPage : mainPageComponent

    Component {
        id: loadingPage
        Kirigami.Page {
            Kirigami.PlaceholderMessage {
                anchors.centerIn: parent
                text: i18n("Loading, please wait...")
            }
        }
    }

This will create a small loading page and will react to the loading signal we created previously.

The next component is the actual UI of the mail folder selector page:

    Component {
        id: mainPageComponent

        Kirigami.ScrollablePage {
            title: i18n("KMailQuick")

            ListView {
                model: QuickMail.descendantsProxyModel
                delegate: Kirigami.BasicListItem {
                    text: model.display
                    leftPadding: Kirigami.Units.gridUnit * model.kDescendantLevel
                    onClicked: {
                        QuickMail.loadMailCollection(model.index);
                        root.pageStack.push(folderPageComponent, {
                            title: model.display
                        });
                    }
                }
            }
        }
    }

Screenshot of a tree view of mail folders

The List of Mails

The next step in our minimal mail client is implementing the inbox view.

Let's go back to the QuickMail constructor and add a mail list model. We use a QItemSelectionModel with an Akonadi::SelectionProxyModel to handle the selection of a folder and loading its content.

    // Setup selection model
    m_collectionSelectionModel = new QItemSelectionModel(entityTreeModel);
    auto selectionModel = new SelectionProxyModel(m_collectionSelectionModel, this);
    selectionModel->setSourceModel(treeModel);
    selectionModel->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection);

This requires adding a new property to the QuickMail class: QItemSelectionModel *m_collectionSelectionModel;

The selectionModel will include all the items inside the selected collections. This might not only include emails so we need to filter it with an EntityMimeTypeFilterModel. This is a simple proxy model that filters according to inclusion and exclusion based on MIME types. We only need to display emails ("message/rfc822") and also tell the proxy model to exclude sub-collections.

    // Setup mail model
    auto folderFilterModel = new EntityMimeTypeFilterModel(this);
    folderFilterModel->setSourceModel(selectionModel);
    folderFilterModel->setHeaderGroup(EntityTreeModel::ItemListHeaders);
    folderFilterModel->addMimeTypeInclusionFilter(QStringLiteral("message/rfc822"));
    folderFilterModel->addMimeTypeExclusionFilter(Collection::mimeType());

The current model doesn't expose roles to the QML engine so we need to add another proxy model to expose 2 roles to the QML view: title and sender. We will call this model MailModel.

    // Proxy for QML roles
    m_folderModel = new MailModel(this);
    m_folderModel->setSourceModel(folderFilterModel);

    // Loading state change handler
    ...

Let's now create the new MailModel. In src/mailmodel.h, include the following content:

#pragma once
#include <QIdentityProxyModel>

class MailModel : public QIdentityProxyModel
{
    Q_OBJECT
public:
    enum AnimalRoles {
        TitleRole = Qt::UserRole + 1,
        SenderRole,
    };
    explicit MailModel(QObject *parent = nullptr);
    QHash<int, QByteArray> roleNames() const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};

In src/mailmodel.cpp, we add the actual implementation of the proxy model. This is rather straightforward, as an Akonadi collection is a list of Akonadi::Item, with the Item containing the data we are interested in. In the case of emails, it will contain a KMime::Message::Ptr, but for example for calendars, it will contain KCalendarCore::Incidence. Getting the KMime::Message from the Item is simply done using item.payload<KMime::Message::Ptr>(). We can then expose the properties of the message to the model.

QVariant MailModel::data(const QModelIndex &index, int role) const
{
    QVariant itemVariant = sourceModel()->data(mapToSource(index), Akonadi::EntityTreeModel::ItemRole);

    Akonadi::Item item = itemVariant.value<Akonadi::Item>();

    if (!item.hasPayload<KMime::Message::Ptr>()) {
         return QVariant();
    }
    const KMime::Message::Ptr mail = item.payload<KMime::Message::Ptr>();

    switch (role) {
    case TitleRole:
        if (mail->subject()) {
            return mail->subject()->asUnicodeString();
        } else {
            return QStringLiteral("(No subject)");
        }
    case SenderRole:
        if (mail->from()) {
            return mail->from()->asUnicodeString();
        } else {
            return QString();
        }
    }

    return {};
}

Don't forget to register the MailModel in the main.cpp file.

#include "mailmodel.h"

    ...
    qRegisterMetaType<MailModel*>("MailModel*");

Selecting a collection

The model initialization is now done and the last remaining part left to create in the C++ code is handling when a user clicks on a mail folder. For this we add a new method to QuickMail: Q_INVOKABLE loadMailCollection(const int &index);. This implementation calls select with the original folder's index inside the treeModel.

void QuickMail::loadMailCollection(const int &index)
{
    QModelIndex flatIndex = m_descendantsProxyModel->index(index, 0);
    QModelIndex modelIndex = m_descendantsProxyModel->mapToSource(flatIndex);

    if (!modelIndex.isValid()) {
        return;
    }

    m_collectionSelectionModel->select(modelIndex, QItemSelectionModel::ClearAndSelect);
}

The User Interface

Building upon our previously built UI, we will fill the onClicked handler for the folderListView. This will call loadMailCollection we created previously to load the emails and then create the view containing the list of emails.

    onClicked: {
        QuickMail.loadMailCollection(model.index);
        root.pageStack.push(folderPageComponent, {
            title: model.display
        });
    }

The list of mail component is a simple ListView using the QuickMail.folderModel model.

And finally, the last component displays the list of emails.

    Component {
        id: folderPageComponent

        Kirigami.ScrollablePage {
            ListView {
                id: mails
                model: QuickMail.folderModel
                delegate: Kirigami.BasicListItem {
                    label: model.title
                    subtitle: sender
                    onClicked: {
                        root.pageStack.push(mailComponent, {
                            'mail': model.mail
                        });
                    }
                }
            }
        }
    }

List of emails

Making the Model Faster

You've probably noticed that when the application loads, it often freezes for a few seconds. This is because we are using a simple monitor that loads everything, including all the data we don't need. To make it faster, we should only load the information we need to display the mail list: the subject, the sender and the date. We could configure the monitor ourselves, but instead, we will use the premade MailCommon::FolderCollectionMonitor from the MailCommon library.

#include <MailCommon/FolderCollectionMonitor>

QuickMail::QuickMail(QObject *parent)

    ...

    m_session = new Session(QByteArrayLiteral("KQuickMail Kernel"), this);
    auto folderCollectionMonitor = new MailCommon::FolderCollectionMonitor(session, this);

    auto entityTreeModel = new Akonadi::CollectionFilterProxyModel();
    entityTreeModel->setSourceModel(treeModel);
    entityTreeModel->addMimeTypeFilter(KMime::Message::mimeType());

We also need to add MailCommon to our dependencies now.

# CMakeLists.txt
find_package(KF5MailCommon ${LIBKDEPIM_VERSION} CONFIG REQUIRED)
# src/CMakeLists.txt
target_link_libraries(kmailquick
    ...
    KF5::MailCommon
)

A Mail Viewer

The last (and probably most important) feature in an email viewer is displaying emails. Before using FolderCollectionMonitor, we could have simply called message->textContent()->decodedText() on a KMime::Message to get the content of an email. Unfortunately, now that we are only fetching the minimal information that we require from Akonadi to display an email list, this isn't possible anymore. Instead, we'll need to fetch the email's content on-demand.

To make our code a bit cleaner, we will wrap the mail content inside a new class: MailWrapper. This class will be responsible for fetching all the information about the mail we want to display.

class MessageWrapper : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString from READ from NOTIFY loaded);
    Q_PROPERTY(QStringList to READ to NOTIFY loaded);
    Q_PROPERTY(QStringList cc READ cc NOTIFY loaded);
    Q_PROPERTY(QString sender READ sender NOTIFY loaded);
    Q_PROPERTY(QString subject READ subject NOTIFY loaded);
    Q_PROPERTY(QDateTime date READ date NOTIFY loaded);
    Q_PROPERTY(QString content READ content NOTIFY loaded);
public:
    explicit MessageWrapper(const Akonadi::Item &item, QObject *parent = nullptr);

    QString from() const;
    QStringList to() const;
    QStringList cc() const;
    QString sender() const;
    QString subject() const;
    QDateTime date() const;
    QString content() const;

Q_SIGNALS:
    void loaded();

private:
    Akonadi::ItemFetchJob *createFetchJob(const Akonadi::Item &item);
    Akonadi::Item m_item;
    KMime::Message::Ptr m_mail;
};

The constructor of the MessageWrapper will fetch the content of the message in case the Item is empty. It's using a Akonadi::ItemFetchJob that is created inside the createFetchJob method. When we get the full item, we emit a loaded signal to update the UI.

// src/messagewrapper.cpp
#include "messagewrapper.h"

#include "quickmail.h"

#include <KLocalizedString>
#include <Akonadi/KMime/MessageParts>
#include <Session>
#include <MailTransportAkonadi/ErrorAttribute>
#include <ItemFetchJob>
#include <ItemFetchScope>
#include <algorithm>
#include <QDebug>

MessageWrapper::MessageWrapper(const Akonadi::Item &item, QObject *parent)
    : QObject(parent)
    , m_item(item)
{
    if (!item.isValid() || item.loadedPayloadParts().contains(Akonadi::MessagePart::Body)) {
        m_mail = item.payload<KMime::Message::Ptr>();
        Q_EMIT loaded();
    } else {
        m_mail = QSharedPointer<KMime::Message>::create();
        Akonadi::ItemFetchJob *job = createFetchJob(item);
        connect(job, &Akonadi::ItemFetchJob::result, [this](KJob *job) {
            if (job->error()) {
                // TODO
            } else {
                auto fetch = qobject_cast<Akonadi::ItemFetchJob *>(job);
                Q_ASSERT(fetch);
                if (fetch->items().isEmpty()) {
                    // TODO display mssage not found error
                } else {
                    m_mail = fetch->items().constFirst().payload<KMime::Message::Ptr>();
                    Q_EMIT loaded();
                }
            }
        });
    }
}

In createFetchJob, we need to create the ItemFetchJob and define its scope. The scope specifies which parts of an item should be fetched from Akonadi. We ask for the full payload, the parent collection, and possible related content.

Akonadi::ItemFetchJob *MessageWrapper::createFetchJob(const Akonadi::Item &item)
{
    auto job = new Akonadi::ItemFetchJob(item, quickMail);
    job->fetchScope().fetchAllAttributes();
    job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent);
    job->fetchScope().fetchFullPayload(true);
    job->fetchScope().setFetchRelations(true); // needed to know if we have notes or not
    job->fetchScope().fetchAttribute<MailTransport::ErrorAttribute>();
    return job;
}

Reading the content of the mail is then done by calling the appropriate methods from the KMime::Message. Similarly, the other properties can be implemented as wrappers around KMime::Message. For more details, you can take a look at the complete implementation.

QString MessageWrapper::content() const
{
    const auto plain = m_mail->mainBodyPart("text/plain");
    if (plain) {
        return plain->decodedText();
    }
    return m_mail->textContent()->decodedText();
}

To make this work, we need to transform the QuickMail class into a singleton, since we need to access the session when creating a job.

// src/quickmail.h

class QuickMail : public QObject
{
    ...
}

Q_GLOBAL_STATIC(QuickMail, quickMail)

Then, in mail.cpp, we change how QuickMail is exposed to the QML engine.

// src/main.cpp

int main(int argc, char *argv[])
{
    ...
    qmlRegisterSingletonInstance<QuickMail>("org.kde.quickmail.private", 1, 0, "QuickMail", quickMail);

Finally we need to create the wrapper and for that, we create a new role in MailModel called "mail".

    case MailRole:
        {
            auto wrapper = new MessageWrapper(item);
            QQmlEngine::setObjectOwnership(wrapper, QQmlEngine::JavaScriptOwnership);
            return QVariant::fromValue(wrapper);
        }

The User Interface

In the previously included folderPageComponent, we can now fill the onClicked handler:

    root.pageStack.push(mailComponent, {
        'mail': model.mail
    });

The mail viewer component is also a Kirigami.ScrollablePage, and we use a TextArea component to display the content.

    Component {
        id: mailComponent

        Kirigami.ScrollablePage {
            required property var mail
            title: mail.subject

            ColumnLayout {
                Kirigami.FormLayout {
                    Layout.fillWidth: true
                    Controls.Label {
                        Kirigami.FormData.label: i18n("From:")
                        text: mail.from
                    }
                    Controls.Label {
                        Kirigami.FormData.label: i18n("To:")
                        text: mail.to.join(', ')
                    }
                    Controls.Label {
                        visible: mail.sender !== mail.from && mail.sender.length > 0
                        Kirigami.FormData.label: i18n("Sender:")
                        text: mail.sender
                    }
                    Controls.Label {
                        Kirigami.FormData.label: i18n("Date:")
                        text: mail.date.toLocaleDateString()
                    }
                }
                Kirigami.Separator {
                    Layout.fillWidth: true
                }
                Controls.TextArea {
                    background: Item {}
                    textFormat: TextEdit.AutoText
                    Layout.fillWidth: true
                    readOnly: true
                    selectByMouse: true
                    text: mail.content
                    wrapMode: Text.Wrap
                }
            }
        }

Mail view

Downloading the project

This tutorial glossed over some of the more technical details. If you are interested in poking around, you can download a complete and working version of the example here.