Mailcommon

filteractionencrypt.cpp
1/*
2 * SPDX-FileCopyrightText: 2017 Daniel Vrátil <dvratil@kde.org>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 *
6 */
7
8#include "filteractionencrypt.h"
9#include "mailcommon_debug.h"
10#include "util/cryptoutils.h"
11
12#include <QCheckBox>
13#include <QEventLoop>
14#include <QLabel>
15#include <QVBoxLayout>
16
17#include <KLocalizedString>
18
19#include <KMime/Message>
20
21#include <QGpgME/EncryptJob>
22#include <QGpgME/KeyListJob>
23#include <QGpgME/ListAllKeysJob>
24#include <QGpgME/Protocol>
25#include <gpgme++/encryptionresult.h>
26#include <gpgme++/keylistresult.h>
27
28#include <Libkleo/DefaultKeyFilter>
29#include <Libkleo/KeySelectionCombo>
30
31#include <MessageComposer/EncryptJob>
32
33#include <Akonadi/MessageFlags>
34
35#include <KColorScheme>
36
37using namespace MailCommon;
38
39#define LISTING_FINISHED "listingFinished"
40#define IGNORE_KEY_CHANGE "ignoreKeyChange"
41
42FilterActionEncrypt::FilterActionEncrypt(QObject *parent)
43 : FilterActionWithCrypto(QStringLiteral("encrypt"), i18n("Encrypt"), parent)
44 , mKeyCache(Kleo::KeyCache::instance())
45{
46}
47
48FilterActionEncrypt::~FilterActionEncrypt() = default;
49
50FilterAction *FilterActionEncrypt::newAction()
51{
52 return new FilterActionEncrypt();
53}
54
55QString FilterActionEncrypt::displayString() const
56{
57 return label();
58}
59
60QString FilterActionEncrypt::argsAsString() const
61{
62 if (mKey.isNull()) {
63 return {};
64 }
65
66 const auto proto = ((mKey.protocol() == GpgME::OpenPGP) ? QStringLiteral("PGP") : QStringLiteral("SMIME"));
67 return QStringLiteral("%1:%2:%3").arg(proto, QString::number(int(mReencrypt)), QString::fromLatin1(mKey.primaryFingerprint()));
68}
69
70void FilterActionEncrypt::argsFromString(const QString &argsStr)
71{
72 const int pos = argsStr.indexOf(QLatin1Char(':'));
73 const QStringView strView(argsStr);
74 const auto protoStr = strView.left(pos);
75
76 QGpgME::Protocol *proto = {};
77 if (protoStr == QLatin1StringView("PGP")) {
78 proto = QGpgME::openpgp();
79 } else if (protoStr == QLatin1StringView("SMIME")) {
80 proto = QGpgME::smime();
81 } else {
82 qCWarning(MAILCOMMON_LOG) << "Unknown protocol specified:" << protoStr;
83 return;
84 }
85 mReencrypt = static_cast<bool>(QStringView(argsStr).mid(pos + 1, 1).toInt());
86
87 const auto fp = argsStr.mid(pos + 3);
88 auto listJob = proto->keyListJob(false, true, true);
89
90 std::vector<GpgME::Key> keys;
91 auto result = listJob->exec({fp}, true, keys);
92 listJob->deleteLater();
93
94 if (result.error()) {
95 qCWarning(MAILCOMMON_LOG) << "Failed to retrieve keys:" << result.error().asString();
96 return;
97 }
98
99 if (keys.empty()) {
100 qCWarning(MAILCOMMON_LOG) << "Could not obtain configured key: key expired or removed?";
101 // TODO: Interactively ask user to re-configure the filter
102 return;
103 }
104
105 mKey = keys[0];
106}
107
108SearchRule::RequiredPart FilterActionEncrypt::requiredPart() const
109{
111}
112
113FilterAction::ReturnCode FilterActionEncrypt::process(ItemContext &context, bool) const
114{
115 if (mKey.isNull()) {
116 qCWarning(MAILCOMMON_LOG) << "FilterActionEncrypt::process called without filter having a key!";
117 return ErrorButGoOn;
118 }
119
120 auto &item = context.item();
121 if (!item.hasPayload<KMime::Message::Ptr>()) {
122 qCWarning(MAILCOMMON_LOG) << "Item" << item.id() << "does not contain KMime::Message payload!";
123 return ErrorNeedComplete;
124 }
125
126 auto msg = item.payload<KMime::Message::Ptr>();
127 if (KMime::isEncrypted(msg.data())) {
128 if (mReencrypt) {
129 // Make sure the email is not already encrypted by the mKey - this is
130 // a little expensive, but still much cheaper than modifying and
131 // re-uploading the email to the server
132 const auto encryptionKeys = getEncryptionKeysFromContent(msg, mKey.protocol());
133 qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "encrypted by following keys: " << encryptionKeys;
134 if (!encryptionKeys.isEmpty()) {
135 if (mKey.protocol() == GpgME::OpenPGP) {
136 std::vector<std::string> ids;
137 ids.reserve(encryptionKeys.size());
138 for (const auto &key : encryptionKeys) {
139 ids.push_back(key.toStdString());
140 }
141 for (const auto &key : mKeyCache->findByKeyIDOrFingerprint(ids)) {
142 if (qstrcmp(key.primaryFingerprint(), mKey.primaryFingerprint()) == 0) {
143 // This email is already encrypted with the target key,
144 // so there's no need to re-encrypt it
145 qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "already encrypted with" << mKey.primaryFingerprint() << ", not re-encrypting";
146 return GoOn;
147 }
148 }
149 } else if (mKey.protocol() == GpgME::CMS) {
150 // We are only able to get serial
151 for (const auto &key : mKeyCache->secretKeys()) {
152 if (qstrcmp(key.issuerSerial(), mKey.issuerSerial()) == 0) {
153 // This email is already encrypted with the target key,
154 // so there's no need to re-encrypt it
155 qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "already encrypted with" << mKey.primaryFingerprint() << ", not re-encrypting";
156 return GoOn;
157 }
158 }
159 }
160 }
161 bool dummy; // dummy
162 const auto decrypted = CryptoUtils::decryptMessage(msg, dummy);
163 if (!decrypted) {
164 // We failed to decrypt the encrypted email - very likely we just don't
165 // have the right key, so don't consider it an error
166 return GoOn;
167 } else {
168 msg = decrypted;
169 }
170 } else {
171 return GoOn;
172 }
173 }
174
176 encrypt.setContent(msg.data());
177 encrypt.setCryptoMessageFormat(mKey.protocol() == GpgME::OpenPGP ? Kleo::OpenPGPMIMEFormat : Kleo::SMIMEFormat);
178 encrypt.setEncryptionKeys({mKey});
179 encrypt.exec();
180 if (encrypt.error()) {
181 qCWarning(MAILCOMMON_LOG) << "Encryption error:" << encrypt.errorString();
182 return ErrorButGoOn;
183 }
184
185 KMime::Content *result = encrypt.content();
186 result->assemble();
187
188 auto nec = CryptoUtils::assembleMessage(msg, result);
189 context.item().setPayload(nec);
191 context.setNeedsPayloadStore();
192 context.setNeedsFlagStore();
193
194 delete result;
195
196 return GoOn;
197}
198
199bool FilterActionEncrypt::isEmpty() const
200{
201 return mKey.isNull();
202}
203
204QString FilterActionEncrypt::informationAboutNotValidAction() const
205{
206 return i18n("No encryption key has been selected");
207}
208
209QWidget *FilterActionEncrypt::createParamWidget(QWidget *parent) const
210{
211 auto w = new QWidget(parent);
212 auto l = new QVBoxLayout;
213 w->setLayout(l);
214
215 auto combo = new Kleo::KeySelectionCombo(w);
216 combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint()));
217
218 std::shared_ptr<Kleo::DefaultKeyFilter> filter(new Kleo::DefaultKeyFilter);
219 filter->setIsOpenPGP(Kleo::DefaultKeyFilter::DoesNotMatter);
220 filter->setCanEncrypt(Kleo::DefaultKeyFilter::Set);
221 filter->setHasSecret(Kleo::DefaultKeyFilter::Set);
222 combo->setKeyFilter(filter);
223
224 combo->setProperty(LISTING_FINISHED, false);
225 combo->setProperty(IGNORE_KEY_CHANGE, false);
226 connect(combo, &Kleo::KeySelectionCombo::keyListingFinished, combo, [combo] {
227 combo->setProperty(LISTING_FINISHED, true);
228 combo->setProperty(IGNORE_KEY_CHANGE, true);
229 });
230 connect(combo, &Kleo::KeySelectionCombo::currentKeyChanged, this, [this, combo]() {
231 // Ignore key change due to the combo box populating itself after
232 // finish
233 if (!combo->property(IGNORE_KEY_CHANGE).toBool()) {
234 Q_EMIT const_cast<FilterActionEncrypt *>(this)->filterActionModified();
235 } else {
236 combo->setProperty(IGNORE_KEY_CHANGE, false);
237 }
238 });
239 l->addWidget(combo);
240
241 auto chkBox = new QCheckBox(w);
242 chkBox->setText(i18n("Re-encrypt encrypted emails with this key"));
243 chkBox->setChecked(mReencrypt);
245 l->addWidget(chkBox);
246
247 auto lbl = new QLabel(w);
248 auto palette = lbl->palette();
249 palette.setColor(lbl->foregroundRole(), KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText).color());
250 lbl->setPalette(palette);
251 lbl->setWordWrap(true);
252 lbl->setText(i18n("<b>Warning:</b> Seckey necessary to read emails."));
253 lbl->setToolTip(
254 i18n("<p>Once an email has been encrypted you will need a crypto setup with "
255 "your secret key to access the contents again.</p>"
256 "<p>If you keep emails stored on an email server and use several clients, "
257 "each of them must be configured to enable decryption.</p>"));
258 l->addWidget(lbl);
259
260 return w;
261}
262
263void FilterActionEncrypt::setParamWidgetValue(QWidget *paramWidget) const
264{
265 if (auto combo = paramWidget->findChild<Kleo::KeySelectionCombo *>()) {
266 combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint()));
267 combo->setCurrentKey(QString::fromLatin1(mKey.primaryFingerprint()));
268 }
269 if (auto chkBox = paramWidget->findChild<QCheckBox *>()) {
270 chkBox->setChecked(mReencrypt);
271 }
272}
273
274void FilterActionEncrypt::applyParamWidgetValue(QWidget *paramWidget)
275{
276 if (auto combo = paramWidget->findChild<Kleo::KeySelectionCombo *>()) {
277 // FIXME: This is super-ugly, but unfortunately the filtering code generates
278 // several instances of this filter and passes the paramWidgets from one
279 // instance to another to "copy" stuff in between, which in our case leads
280 // to this method being called on an un-populated combobox
281 if (!combo->property(LISTING_FINISHED).toBool()) {
282 QEventLoop ev;
283 connect(combo, &Kleo::KeySelectionCombo::keyListingFinished, &ev, &QEventLoop::quit, Qt::QueuedConnection);
284 ev.exec();
285 }
286 mKey = combo->currentKey();
287 }
288 if (auto chkBox = paramWidget->findChild<QCheckBox *>()) {
289 mReencrypt = chkBox->isChecked();
290 }
291}
292
293GpgME::Key FilterActionEncrypt::key() const
294{
295 return mKey;
296}
297
298bool FilterActionEncrypt::reencrypt() const
299{
300 return mReencrypt;
301}
302
303#include "moc_filteractionencrypt.cpp"
void setPayload(const T &p)
Id id() const
void setFlag(const QByteArray &name)
bool exec()
virtual QString errorString() const
int error() const
Abstract base class for mail filter actions.
ReturnCode
Describes the possible return codes of filter processing:
@ ErrorNeedComplete
Could not process because a complete message is needed.
@ ErrorButGoOn
A non-critical error occurred.
@ GoOn
Go on with applying filter actions.
void filterActionModified()
Called to notify that the current FilterAction has had some value modification.
QString label() const
Returns i18n'd label, ie.
A helper class for the filtering process.
Definition itemcontext.h:27
void setNeedsPayloadStore()
Marks that the item's payload has been changed and needs to be written back.
Akonadi::Item & item()
Returns the item of the context.
void setNeedsFlagStore()
Marks that the item's flags has been changed and needs to be written back.
RequiredPart
Possible required parts.
Definition searchrule.h:68
@ CompleteMessage
Whole message.
Definition searchrule.h:71
KMime::Content * content() const
QString i18n(const char *text, const TYPE &arg...)
AKONADI_MIME_EXPORT const char Encrypted[]
The filter dialog.
void toggled(bool checked)
int exec(ProcessEventsFlags flags)
void quit()
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
T findChild(const QString &name, Qt::FindChildOptions options) const const
QObject * parent() const const
QString arg(Args &&... args) const const
QString fromLatin1(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
QStringView mid(qsizetype start, qsizetype length) const const
int toInt(bool *ok, int base) const const
QueuedConnection
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:00 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.