diff --git a/framework/qml/MailListView.qml b/framework/qml/MailListView.qml index 924aeb94..c56b8fe7 100644 --- a/framework/qml/MailListView.qml +++ b/framework/qml/MailListView.qml @@ -1,323 +1,323 @@ /* Copyright (C) 2016 Michael Bohlender, This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.9 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.1 import org.kube.framework 1.0 as Kube FocusScope { id: root //Private properties property variant parentFolder: null property variant currentMail: null property alias filter: mailListModel.filter property alias threaded: mailListModel.threaded onParentFolderChanged: { currentMail = null } Kube.Listener { filter: Kube.Messages.selectTopConversation onMessageReceived: { listView.currentIndex = 0 listView.forceActiveFocus() } } Kube.Listener { filter: Kube.Messages.selectNextConversation onMessageReceived: { listView.incrementCurrentIndex() listView.forceActiveFocus() } } Kube.Listener { filter: Kube.Messages.selectPreviousConversation onMessageReceived: { listView.decrementCurrentIndex() listView.forceActiveFocus() } } Kube.Label { anchors.centerIn: parent visible: listView.count === 0 //TODO depending on whether we synchronized already or not the label should change. text: qsTr("Nothing here...") } ColumnLayout { anchors.fill: parent spacing: 0 Kube.ListView { id: listView objectName: "listView" Layout.fillWidth: true Layout.fillHeight: true clip: true focus: true onActiveFocusChanged: { if (activeFocus && currentIndex < 0) { currentIndex = 0 } } Keys.onPressed: { //Not implemented as a shortcut because we want it only to apply if we have the focus if (event.text == "d" || event.key == Qt.Key_Delete) { Kube.Fabric.postMessage(Kube.Messages.moveToTrash, {"mail": root.currentMail}) } else if (event.text == "r") { Kube.Fabric.postMessage(Kube.Messages.reply, {"mail": root.currentMail}) } else if (event.text == "i") { Kube.Fabric.postMessage(Kube.Messages.setImportant, {"mail": root.currentMail, "important": !currentItem.currentData.important}) } else if (event.text == "u") { Kube.Fabric.postMessage(Kube.Messages.markAsUnread, {"mail": root.currentMail}) } if (event.key == Qt.Key_Home) { listView.currentIndex = 0 } } onCurrentItemChanged: { if (currentItem) { var currentData = currentItem.currentData; root.currentMail = currentData.mail; if (currentData.mail && currentData.unread) { Kube.Fabric.postMessage(Kube.Messages.markAsRead, {"mail": currentData.mail}) } } } model: Kube.MailListModel { id: mailListModel parentFolder: root.parentFolder } delegate: Kube.ListDelegate { id: delegateRoot //Required for D&D property var mail: model.mail property bool buttonsVisible: delegateRoot.hovered width: listView.availableWidth height: Kube.Units.gridUnit * 5 color: Kube.Colors.viewBackgroundColor border.color: Kube.Colors.backgroundColor border.width: 1 states: [ State { name: "dnd" when: mouseArea.drag.active PropertyChanges {target: mouseArea; cursorShape: Qt.ClosedHandCursor} PropertyChanges {target: delegateRoot; x: x; y: y} PropertyChanges {target: delegateRoot; parent: root} PropertyChanges {target: delegateRoot; opacity: 0.2} PropertyChanges {target: delegateRoot; highlighted: true} } ] Drag.active: mouseArea.drag.active Drag.hotSpot.x: mouseArea.mouseX Drag.hotSpot.y: mouseArea.mouseY Drag.source: delegateRoot MouseArea { id: mouseArea anchors.fill: parent drag.target: parent onReleased: { var dropAction = parent.Drag.drop() if (dropAction == Qt.MoveAction) { parent.visible = false } } onClicked: delegateRoot.clicked() } Item { id: content anchors { fill: parent margins: Kube.Units.smallSpacing } property color unreadColor: (model.unread && !delegateRoot.highlighted) ? Kube.Colors.highlightColor : delegateRoot.textColor //TODO batch editing // Kube.CheckBox { // id: checkBox // // anchors.verticalCenter: parent.verticalCenter // visible: (checked || delegateRoot.hovered) && !mouseArea.drag.active // opacity: 0.9 // } Column { anchors { verticalCenter: parent.verticalCenter left: parent.left leftMargin: Kube.Units.largeSpacing // + checkBox.width } Kube.Label{ id: subject width: content.width - Kube.Units.gridUnit * 3 text: model.subject color: content.unreadColor maximumLineCount: 2 wrapMode: Text.WordWrap elide: Text.ElideRight } Kube.Label { id: sender text: model.senderName color: delegateRoot.textColor font.italic: true width: delegateRoot.width - Kube.Units.gridUnit * 3 elide: Text.ElideRight } } Kube.Label { id: date anchors { right: parent.right bottom: parent.bottom } function sameDay(date1, date2) { return date1.getFullYear() == date2.getFullYear() && date1.getMonth() == date2.getMonth() && date1.getDate() == date2.getDate() } function formatDateTime(date) { const today = new Date() if (sameDay(date, today)) { return Qt.formatDateTime(date, "hh:mm") } const lastWeekToday = today.getTime() - ((24*60*60*1000) * 7); if (date.getTime() >= lastWeekToday) { return Qt.formatDateTime(date, "ddd hh:mm") } return Qt.formatDateTime(date, "dd MMM yyyy") } visible: !delegateRoot.buttonsVisible - text: formatDateTime(model.date, "dd MMM yyyy") + text: formatDateTime(model.date) font.italic: true color: delegateRoot.disabledTextColor font.pointSize: Kube.Units.tinyFontSize } Kube.Label { id: threadCounter anchors { right: parent.right margins: Kube.Units.smallSpacing } text: model.threadSize color: content.unreadColor visible: model.threadSize > 1 && !delegateRoot.buttonsVisible } } Kube.Icon { anchors { right: parent.right verticalCenter: parent.verticalCenter margins: Kube.Units.smallSpacing } visible: model.important && !delegateRoot.buttonsVisible && !mouseArea.drag.active iconName: Kube.Icons.isImportant } Column { id: buttons anchors { right: parent.right margins: Kube.Units.smallSpacing verticalCenter: parent.verticalCenter } visible: delegateRoot.buttonsVisible && !mouseArea.drag.active opacity: 0.7 Kube.IconButton { id: restoreButton iconName: Kube.Icons.undo visible: !!model.trash onClicked: Kube.Fabric.postMessage(Kube.Messages.restoreFromTrash, {"mail": model.mail}) activeFocusOnTab: false tooltip: qsTr("Restore from trash") } Kube.IconButton { id: readButton iconName: Kube.Icons.markAsRead visible: model.unread && !model.trash onClicked: Kube.Fabric.postMessage(Kube.Messages.markAsRead, {"mail": model.mail}) tooltip: qsTr("Mark as read") } Kube.IconButton { id: unreadButton iconName: Kube.Icons.markAsUnread visible: !model.unread && !model.trash onClicked: Kube.Fabric.postMessage(Kube.Messages.markAsUnread, {"mail": model.mail}) activeFocusOnTab: false tooltip: qsTr("Mark as unread") } Kube.IconButton { id: importantButton iconName: model.important ? Kube.Icons.markImportant : Kube.Icons.markUnimportant visible: !!model.mail onClicked: Kube.Fabric.postMessage(Kube.Messages.setImportant, {"mail": model.mail, "important": !model.important}) activeFocusOnTab: false tooltip: qsTr("Mark as important") } Kube.IconButton { id: deleteButton objectName: "deleteButton" iconName: Kube.Icons.moveToTrash visible: !!model.mail onClicked: Kube.Fabric.postMessage(Kube.Messages.moveToTrash, {"mail": model.mail}) activeFocusOnTab: false tooltip: qsTr("Move to trash") } } } } } } diff --git a/framework/src/domain/settings/accountsettings.cpp b/framework/src/domain/settings/accountsettings.cpp index c1739738..e9b3c598 100644 --- a/framework/src/domain/settings/accountsettings.cpp +++ b/framework/src/domain/settings/accountsettings.cpp @@ -1,460 +1,460 @@ /* Copyright (c) 2016 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "accountsettings.h" #include #include #include #include #include #include "keyring.h" #include using namespace Sink; using namespace Sink::ApplicationDomain; AccountSettings::AccountSettings(QObject *parent) : QObject(parent) { } void AccountSettings::setAccountType(const QByteArray &type) { mAccountType = type; } QByteArray AccountSettings::accountType() const { return mAccountType; } void AccountSettings::setAccountIdentifier(const QByteArray &id) { if (id.isEmpty()) { return; } mAccountIdentifier = id; //Clear mIcon = QString(); mName = QString(); mImapServer = QString(); mImapUsername = QString(); mSmtpServer = QString(); mSmtpUsername = QString(); mCardDavServer = QString(); mCardDavUsername = QString(); mCalDavServer = QString(); mCalDavUsername = QString(); mPath = QString(); emit changed(); emit imapResourceChanged(); emit smtpResourceChanged(); emit cardDavResourceChanged(); emit calDavResourceChanged(); emit pathChanged(); load(); } QByteArray AccountSettings::accountIdentifier() const { return mAccountIdentifier; } void AccountSettings::setPath(const QUrl &path) { auto normalizedPath = path.path(); if (mPath != normalizedPath) { mPath = normalizedPath; emit pathChanged(); } } QUrl AccountSettings::path() const { return QUrl(mPath); } QValidator *AccountSettings::pathValidator() const { class PathValidator : public QValidator { State validate(QString &input, int &pos) const { Q_UNUSED(pos); if (!input.isEmpty() && QDir(input).exists()) { return Acceptable; } else { return Intermediate; } } }; static PathValidator *pathValidator = new PathValidator; return pathValidator; } QValidator *AccountSettings::imapServerValidator() const { class ImapServerValidator : public QValidator { State validate(QString &input, int &pos) const { Q_UNUSED(pos); // imaps://mainserver.example.net:475 const QUrl url(input); - static QSet validProtocols = QSet() << "imap" << "imaps"; + static QSet validProtocols{{"imap"}, {"imaps"}}; if (url.isValid() && validProtocols.contains(url.scheme().toLower())) { return Acceptable; } else { return Intermediate; } } }; static ImapServerValidator *validator = new ImapServerValidator; return validator; } QValidator *AccountSettings::smtpServerValidator() const { class SmtpServerValidator : public QValidator { State validate(QString &input, int &pos) const { Q_UNUSED(pos); // smtps://mainserver.example.net:475 const QUrl url(input); static QSet validProtocols = QSet() << "smtp" << "smtps"; if (url.isValid() && validProtocols.contains(url.scheme().toLower())) { return Acceptable; } else { return Intermediate; } } }; static SmtpServerValidator *validator = new SmtpServerValidator; return validator; } void AccountSettings::saveAccount() { if (mAccountIdentifier.isEmpty()) { auto account = ApplicationDomainType::createEntity(); mAccountIdentifier = account.identifier(); Q_ASSERT(!mAccountType.isEmpty()); account.setAccountType(mAccountType); account.setName(mName); account.setIcon(mIcon); Store::create(account) .onError([](const KAsync::Error &error) { qWarning() << "Error while creating account: " << error.errorMessage;; }) .exec().waitForFinished(); } else { qDebug() << "Saving account " << mAccountIdentifier; Q_ASSERT(!mAccountIdentifier.isEmpty()); SinkAccount account(mAccountIdentifier); account.setAccountType(mAccountType); account.setName(mName); account.setIcon(mIcon); Q_ASSERT(!account.identifier().isEmpty()); Store::modify(account) .onError([](const KAsync::Error &error) { qWarning() << "Error while creating account: " << error.errorMessage;; }) .exec().waitForFinished(); } } void AccountSettings::loadAccount() { Q_ASSERT(!mAccountIdentifier.isEmpty()); Store::fetchOne(Query().filter(mAccountIdentifier).request().request().request()) .then([this](const SinkAccount &account) { mAccountType = account.getAccountType().toLatin1(); mIcon = account.getIcon(); mName = account.getName(); emit changed(); }).onError([](const KAsync::Error &error) { qWarning() << "Failed to load the account: " << error.errorMessage; }).exec().waitForFinished(); } void AccountSettings::loadImapResource() { Store::fetchOne(Query().filter(mAccountIdentifier).filter("sink.imap")) .then([this](const SinkResource &resource) { mImapIdentifier = resource.identifier(); mImapServer = resource.getProperty("server").toString(); mImapUsername = resource.getProperty("username").toString(); emit imapResourceChanged(); }).onError([](const KAsync::Error &error) { qWarning() << "Failed to load the imap resource: " << error.errorMessage; }).exec().waitForFinished(); } void AccountSettings::loadMaildirResource() { Store::fetchOne(Query().filter(mAccountIdentifier).filter("sink.maildir")) .then([this](const SinkResource &resource) { mMaildirIdentifier = resource.identifier(); mPath = resource.getProperty("path").toString(); emit pathChanged(); }).onError([](const KAsync::Error &error) { SinkWarning() << "Failed to load the maildir resource: " << error.errorMessage; }).exec().waitForFinished(); } void AccountSettings::loadMailtransportResource() { Store::fetchOne(Query().filter(mAccountIdentifier).filter("sink.mailtransport")) .then([this](const SinkResource &resource) { mMailtransportIdentifier = resource.identifier(); mSmtpServer = resource.getProperty("server").toString(); mSmtpUsername = resource.getProperty("username").toString(); emit smtpResourceChanged(); }).onError([](const KAsync::Error &error) { SinkWarning() << "Failed to load the smtp resource: " << error.errorMessage; }).exec().waitForFinished(); } void AccountSettings::loadIdentity() { //FIXME this assumes that we only ever have one identity per account Store::fetchOne(Query().filter(mAccountIdentifier)) .then([this](const Identity &identity) { mIdentityIdentifier = identity.identifier(); mUsername = identity.getName(); mEmailAddress = identity.getAddress(); emit identityChanged(); }).onError([](const KAsync::Error &error) { SinkWarning() << "Failed to load the identity: " << error.errorMessage; }).exec().waitForFinished(); } void AccountSettings::loadCardDavResource() { Store::fetchOne(Query().filter(mAccountIdentifier).filter("sink.carddav")) .then([this](const SinkResource &resource) { mCardDavIdentifier = resource.identifier(); mCardDavServer = resource.getProperty("server").toString(); mCardDavUsername = resource.getProperty("username").toString(); emit cardDavResourceChanged(); }).onError([](const KAsync::Error &error) { qWarning() << "Failed to load the CardDAV resource: " << error.errorMessage; }).exec().waitForFinished(); } void AccountSettings::loadCalDavResource() { Store::fetchOne(Query().filter(mAccountIdentifier).filter("sink.caldav")) .then([this](const SinkResource &resource) { mCalDavIdentifier = resource.identifier(); mCalDavServer = resource.getProperty("server").toString(); mCalDavUsername = resource.getProperty("username").toString(); emit calDavResourceChanged(); }).onError([](const KAsync::Error &error) { qWarning() << "Failed to load the CalDAV resource: " << error.errorMessage; }).exec().waitForFinished(); } template static QByteArray saveResource(const QByteArray &accountIdentifier, const QByteArray &identifier, const std::map &properties) { if (!identifier.isEmpty()) { SinkResource resource(identifier); for (const auto &pair : properties) { resource.setProperty(pair.first, pair.second); } Store::modify(resource) .onError([](const KAsync::Error &error) { SinkWarning() << "Error while modifying resource: " << error.errorMessage; }) .exec().waitForFinished(); } else { auto resource = ResourceType::create(accountIdentifier); auto newIdentifier = resource.identifier(); for (const auto &pair : properties) { resource.setProperty(pair.first, pair.second); } Store::create(resource) .onError([](const KAsync::Error &error) { SinkWarning() << "Error while creating resource: " << error.errorMessage; }) .exec().waitForFinished(); return newIdentifier; } return identifier; } void AccountSettings::saveImapResource() { mImapIdentifier = saveResource(mAccountIdentifier, mImapIdentifier, { {"server", mImapServer}, {"username", mImapUsername} }); } void AccountSettings::saveCardDavResource() { mCardDavIdentifier = saveResource(mAccountIdentifier, mCardDavIdentifier, { {"server", mCardDavServer}, {"username", mCardDavUsername} }); } void AccountSettings::saveCalDavResource() { mCalDavIdentifier = saveResource(mAccountIdentifier, mCalDavIdentifier, { {"server", mCalDavServer}, {"username", mCalDavUsername} }); } void AccountSettings::saveMaildirResource() { mMaildirIdentifier = saveResource(mAccountIdentifier, mMaildirIdentifier, { {"path", mPath}, }); } void AccountSettings::saveMailtransportResource() { mMailtransportIdentifier = saveResource(mAccountIdentifier, mMailtransportIdentifier, { {"server", mSmtpServer}, {"username", mSmtpUsername} }); } void AccountSettings::login(const QVariantMap &secrets) { // We'll attempt to store your account secrets using a key matching the email address. const auto accountSecret = secrets.value("accountSecret").toString(); Store::fetchAll(Query().filter(mAccountIdentifier)) .then([=](const QList &resources) { Kube::AccountKeyring keyring{mAccountIdentifier}; for (const auto &resource : resources) { keyring.addPassword(resource->identifier(), accountSecret); } const auto keys = Crypto::findKeys({{mEmailAddress}}, true); if (!keys.empty()) { qInfo() << "Storing account secrets."; keyring.save(keys); } else { qInfo() << "Failed to find a GPG key for " << mEmailAddress << ". Not storing account secrets."; } }).onError([](const KAsync::Error &error) { qWarning() << "Failed to load any account resources resource: " << error; }).exec(); } void AccountSettings::saveIdentity() { if (!mIdentityIdentifier.isEmpty()) { Identity identity(mIdentityIdentifier); identity.setName(mUsername); identity.setAddress(mEmailAddress); Store::modify(identity) .onError([](const KAsync::Error &error) { SinkWarning() << "Error while modifying identity: " << error.errorMessage; }) .exec().waitForFinished(); } else { auto identity = ApplicationDomainType::createEntity(); mIdentityIdentifier = identity.identifier(); identity.setAccount(mAccountIdentifier); identity.setName(mUsername); identity.setAddress(mEmailAddress); Store::create(identity) .onError([](const KAsync::Error &error) { SinkWarning() << "Error while creating identity: " << error.errorMessage; }) .exec().waitForFinished(); } } void AccountSettings::removeResource(const QByteArray &identifier) { if (identifier.isEmpty()) { SinkWarning() << "We're missing an identifier"; } else { SinkResource resource(identifier); Store::remove(resource) .onError([](const KAsync::Error &error) { SinkWarning() << "Error while removing resource: " << error.errorMessage; }) .exec().waitForFinished(); } } void AccountSettings::removeAccount() { if (mAccountIdentifier.isEmpty()) { SinkWarning() << "We're missing an identifier"; } else { SinkAccount account(mAccountIdentifier); Store::remove(account) .onError([](const KAsync::Error &error) { SinkWarning() << "Error while removing account: " << error.errorMessage; }) .exec().waitForFinished(); } } void AccountSettings::removeIdentity() { if (mIdentityIdentifier.isEmpty()) { SinkWarning() << "We're missing an identifier"; } else { Identity identity(mIdentityIdentifier); Store::remove(identity) .onError([](const KAsync::Error &error) { SinkWarning() << "Error while removing identity: " << error.errorMessage; }) .exec().waitForFinished(); } } void AccountSettings::load() { loadAccount(); loadImapResource(); loadMailtransportResource(); loadCardDavResource(); loadCalDavResource(); loadIdentity(); } void AccountSettings::save() { saveAccount(); saveImapResource(); saveMailtransportResource(); saveCardDavResource(); saveCalDavResource(); saveIdentity(); } void AccountSettings::remove() { removeResource(mMailtransportIdentifier); removeResource(mImapIdentifier); removeResource(mCardDavIdentifier); removeResource(mCalDavIdentifier); removeIdentity(); removeAccount(); }