Mailcommon

filteractionencrypt.cpp
1 /*
2  * SPDX-FileCopyrightText: 2017 Daniel Vrátil <[email protected]>
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 
37 using namespace MailCommon;
38 
39 #define LISTING_FINISHED "listingFinished"
40 #define IGNORE_KEY_CHANGE "ignoreKeyChange"
41 
42 FilterActionEncrypt::FilterActionEncrypt(QObject *parent)
43  : FilterActionWithCrypto(QStringLiteral("encrypt"), i18n("Encrypt"), parent)
44  , mKeyCache(Kleo::KeyCache::instance())
45 {
46 }
47 
48 FilterActionEncrypt::~FilterActionEncrypt() = default;
49 
50 FilterAction *FilterActionEncrypt::newAction()
51 {
52  return new FilterActionEncrypt();
53 }
54 
55 QString FilterActionEncrypt::displayString() const
56 {
57  return label();
58 }
59 
60 QString 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 
70 void 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 == QLatin1String("PGP")) {
78  proto = QGpgME::openpgp();
79  } else if (protoStr == QLatin1String("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 
108 SearchRule::RequiredPart FilterActionEncrypt::requiredPart() const
109 {
111 }
112 
113 FilterAction::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 
199 bool FilterActionEncrypt::isEmpty() const
200 {
201  return mKey.isNull();
202 }
203 
204 QString FilterActionEncrypt::informationAboutNotValidAction() const
205 {
206  return i18n("No encryption key has been selected");
207 }
208 
209 QWidget *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);
244  connect(chkBox, &QCheckBox::toggled, this, &FilterActionEncrypt::filterActionModified);
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 
263 void 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 
274 void 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 
293 GpgME::Key FilterActionEncrypt::key() const
294 {
295  return mKey;
296 }
297 
298 bool FilterActionEncrypt::reencrypt() const
299 {
300  return mReencrypt;
301 }
QString number(int n, int base)
QStringView mid(qsizetype start) const const
int exec(QEventLoop::ProcessEventsFlags flags)
void toggled(bool checked)
Abstract base class for mail filter actions.
Definition: filteraction.h:38
QString i18n(const char *text, const TYPE &arg...)
void quit()
void setNeedsFlagStore()
Marks that the item's flags has been changed and needs to be written back.
Definition: itemcontext.cpp:43
RequiredPart
Possible required parts.
Definition: searchrule.h:68
KMime::Content * content() const
QFuture< void > filter(Sequence &sequence, KeepFunctor filterFunction)
QueuedConnection
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
void setNeedsPayloadStore()
Marks that the item's payload has been changed and needs to be written back.
Definition: itemcontext.cpp:33
T findChild(const QString &name, Qt::FindChildOptions options) const const
A helper class for the filtering process.
Definition: itemcontext.h:26
QString label(StandardShortcut id)
ReturnCode
Describes the possible return codes of filter processing:
Definition: filteraction.h:45
Id id() const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString fromLatin1(const char *str, int size)
@ CompleteMessage
Whole message.
Definition: searchrule.h:71
QString mid(int position, int n) const const
const AKONADI_MIME_EXPORT char Encrypted[]
void setFlag(const QByteArray &name)
Akonadi::Item & item()
Returns the item of the context.
Definition: itemcontext.cpp:18
The filter dialog.
void setPayload(const T &p)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Mon May 8 2023 03:58:16 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.