Messagelib

storagemodel.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Kevin Ottens <[email protected]>
3 
4  SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "storagemodel.h"
8 
9 #include <MessageCore/MessageCoreSettings>
10 #include <MessageCore/NodeHelper>
11 #include <MessageCore/StringUtil>
12 
13 #include <Akonadi/AttributeFactory>
14 #include <Akonadi/CollectionStatistics>
15 #include <Akonadi/EntityMimeTypeFilterModel>
16 #include <Akonadi/EntityTreeModel>
17 #include <Akonadi/ItemModifyJob>
18 #include <Akonadi/MessageFolderAttribute>
19 #include <Akonadi/SelectionProxyModel>
20 
21 #include "core/messageitem.h"
22 #include "messagelist_debug.h"
23 #include "messagelistsettings.h"
24 #include "messagelistutil.h"
25 #include <KLocalizedString>
26 #include <QUrl>
27 
28 #include <QAbstractItemModel>
29 #include <QAtomicInt>
30 #include <QCryptographicHash>
31 #include <QFontDatabase>
32 #include <QHash>
33 #include <QItemSelectionModel>
34 #include <QMimeData>
35 
36 namespace MessageList
37 {
38 class Q_DECL_HIDDEN StorageModel::StorageModelPrivate
39 {
40 public:
41  void onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
42  void onSelectionChanged();
43  void loadSettings();
44 
45  StorageModel *const q;
46 
47  QAbstractItemModel *mModel = nullptr;
48  QAbstractItemModel *mChildrenFilterModel = nullptr;
49  QItemSelectionModel *mSelectionModel = nullptr;
50 
52 
53  StorageModelPrivate(StorageModel *owner)
54  : q(owner)
55  {
56  }
57 };
58 } // namespace MessageList
59 
60 using namespace Akonadi;
61 using namespace MessageList;
62 
63 namespace
64 {
65 KMime::Message::Ptr messageForItem(const Akonadi::Item &item)
66 {
67  if (!item.hasPayload<KMime::Message::Ptr>()) {
68  qCWarning(MESSAGELIST_LOG) << "Not a message" << item.id() << item.remoteId() << item.mimeType();
69  return {};
70  }
71  return item.payload<KMime::Message::Ptr>();
72 }
73 }
74 
75 static QAtomicInt _k_attributeInitialized;
76 
78  : Core::StorageModel(parent)
79  , d(new StorageModelPrivate(this))
80 {
81  d->mSelectionModel = selectionModel;
82  if (_k_attributeInitialized.testAndSetAcquire(0, 1)) {
83  AttributeFactory::registerAttribute<MessageFolderAttribute>();
84  }
85 
86  auto childrenFilter = new Akonadi::SelectionProxyModel(d->mSelectionModel, this);
87  childrenFilter->setSourceModel(model);
88  childrenFilter->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection);
89  d->mChildrenFilterModel = childrenFilter;
90 
91  auto itemFilter = new EntityMimeTypeFilterModel(this);
92  itemFilter->setSourceModel(childrenFilter);
93  itemFilter->addMimeTypeExclusionFilter(Collection::mimeType());
94  itemFilter->addMimeTypeInclusionFilter(QStringLiteral("message/rfc822"));
95  itemFilter->setHeaderGroup(EntityTreeModel::ItemListHeaders);
96 
97  d->mModel = itemFilter;
98 
99  qCDebug(MESSAGELIST_LOG) << "Using model:" << model->metaObject()->className();
100 
101  connect(d->mModel, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &id1, const QModelIndex &id2) {
102  d->onSourceDataChanged(id1, id2);
103  });
104 
109 
110  // Here we assume we'll always get QModelIndex() in the parameters
115 
116  connect(d->mSelectionModel, &QItemSelectionModel::selectionChanged, this, [this]() {
117  d->onSelectionChanged();
118  });
119 
120  d->loadSettings();
121  connect(MessageListSettings::self(), &MessageListSettings::configChanged, this, [this]() {
122  d->loadSettings();
123  });
124 }
125 
126 MessageList::StorageModel::~StorageModel() = default;
127 
128 Collection::List MessageList::StorageModel::displayedCollections() const
129 {
130  Collection::List collections;
131  const QModelIndexList indexes = d->mSelectionModel->selectedRows();
132 
133  collections.reserve(indexes.count());
134  for (const QModelIndex &index : indexes) {
135  auto c = index.data(EntityTreeModel::CollectionRole).value<Collection>();
136  if (c.isValid()) {
137  collections << c;
138  }
139  }
140 
141  return collections;
142 }
143 
145 {
146  QStringList ids;
147  const QModelIndexList indexes = d->mSelectionModel->selectedRows();
148 
149  ids.reserve(indexes.count());
150  for (const QModelIndex &index : indexes) {
151  auto c = index.data(EntityTreeModel::CollectionRole).value<Collection>();
152  if (c.isValid()) {
153  ids << QString::number(c.id());
154  }
155  }
156 
157  ids.sort();
158  return ids.join(QLatin1Char(':'));
159 }
160 
161 bool MessageList::StorageModel::isOutBoundFolder(const Akonadi::Collection &c) const
162 {
164  return true;
165  }
166  return false;
167 }
168 
170 {
171  const QModelIndexList indexes = d->mSelectionModel->selectedRows();
172 
173  for (const QModelIndex &index : indexes) {
174  auto c = index.data(EntityTreeModel::CollectionRole).value<Collection>();
175  if (c.isValid()) {
176  return isOutBoundFolder(c);
177  }
178  }
179 
180  return false;
181 }
182 
184 {
185  const QModelIndexList indexes = d->mSelectionModel->selectedRows();
186 
187  int unreadCount = 0;
188 
189  for (const QModelIndex &index : indexes) {
190  auto c = index.data(EntityTreeModel::CollectionRole).value<Collection>();
191  if (c.isValid()) {
192  unreadCount += c.statistics().unreadCount();
193  }
194  }
195 
196  return unreadCount;
197 }
198 
200 {
201  const Akonadi::Item item = itemForRow(row);
202  const KMime::Message::Ptr mail = messageForItem(item);
203  if (!mail) {
204  return false;
205  }
206 
207  const Collection parentCol = parentCollectionForRow(row);
208 
209  QString sender;
210  if (mail->from()) {
211  sender = mail->from()->asUnicodeString();
212  }
213  QString receiver;
214  if (mail->to()) {
215  receiver = mail->to()->asUnicodeString();
216  }
217 
218  // Static for speed reasons
219  static const QString noSubject = i18nc("displayed as subject when the subject of a mail is empty", "No Subject");
220  static const QString unknown(i18nc("displayed when a mail has unknown sender, receiver or date", "Unknown"));
221 
222  if (sender.isEmpty()) {
223  sender = unknown;
224  }
225  if (receiver.isEmpty()) {
226  receiver = unknown;
227  }
228 
229  mi->initialSetup(mail->date()->dateTime().toSecsSinceEpoch(), item.size(), sender, receiver, bUseReceiver);
230  mi->setItemId(item.id());
231  mi->setParentCollectionId(parentCol.id());
232 
233  QString subject = mail->subject()->asUnicodeString();
234  if (subject.isEmpty()) {
235  subject = QLatin1Char('(') + noSubject + QLatin1Char(')');
236  }
237 
238  mi->setSubject(subject);
239 
240  auto it = d->mFolderHash.find(item.storageCollectionId());
241  if (it == d->mFolderHash.end()) {
242  QString folder;
243  Collection collection = collectionForId(item.storageCollectionId());
244  while (collection.parentCollection().isValid()) {
245  folder = collection.displayName() + QLatin1Char('/') + folder;
246  collection = collection.parentCollection();
247  }
248  folder.chop(1);
249  it = d->mFolderHash.insert(item.storageCollectionId(), folder);
250  }
251  mi->setFolder(it.value());
252 
253  updateMessageItemData(mi, row);
254 
255  return true;
256 }
257 
258 static QByteArray md5Encode(const QByteArray &str)
259 {
260  auto trimmed = str.trimmed();
261  if (trimmed.isEmpty()) {
262  return {};
263  }
264 
266  c.addData(trimmed);
267  return c.result();
268 }
269 
270 static QByteArray md5Encode(const QString &str)
271 {
272  auto trimmed = str.trimmed();
273  if (trimmed.isEmpty()) {
274  return {};
275  }
276 
278 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
279  c.addData(reinterpret_cast<const char *>(trimmed.unicode()), sizeof(QChar) * trimmed.length());
280 #else
281  c.addData(QByteArrayView(reinterpret_cast<const char *>(trimmed.unicode()), sizeof(QChar) * trimmed.length()));
282 #endif
283  return c.result();
284 }
285 
287 {
288  const KMime::Message::Ptr mail = messageForRow(row);
289  Q_ASSERT(mail); // We ASSUME that initializeMessageItem has been called successfully...
290 
291  switch (subset) {
292  case PerfectThreadingReferencesAndSubject: {
293  const QString subject = mail->subject()->asUnicodeString();
294  const QString strippedSubject = MessageCore::StringUtil::stripOffPrefixes(subject);
295  mi->setStrippedSubjectMD5(md5Encode(strippedSubject));
296  mi->setSubjectIsPrefixed(subject != strippedSubject);
297  // fall through
298  }
299  Q_FALLTHROUGH();
300  case PerfectThreadingPlusReferences: {
301  const auto refs = mail->references()->identifiers();
302  if (!refs.isEmpty()) {
303  mi->setReferencesIdMD5(md5Encode(refs.last()));
304  }
305  }
306  Q_FALLTHROUGH();
307  // fall through
308  case PerfectThreadingOnly: {
309  mi->setMessageIdMD5(md5Encode(mail->messageID()->identifier()));
310  const auto inReplyTos = mail->inReplyTo()->identifiers();
311  if (!inReplyTos.isEmpty()) {
312  mi->setInReplyToIdMD5(md5Encode(inReplyTos.first()));
313  }
314  break;
315  }
316  default:
317  Q_ASSERT(false); // should never happen
318  break;
319  }
320 }
321 
323 {
324  const Akonadi::Item item = itemForRow(row);
325 
327  stat.setStatusFromFlags(item.flags());
328 
329  mi->setAkonadiItem(item);
330  mi->setStatus(stat);
331 
332  if (stat.isEncrypted()) {
333  mi->setEncryptionState(Core::MessageItem::FullyEncrypted);
334  } else {
335  mi->setEncryptionState(Core::MessageItem::EncryptionStateUnknown);
336  }
337 
338  if (stat.isSigned()) {
339  mi->setSignatureState(Core::MessageItem::FullySigned);
340  } else {
341  mi->setSignatureState(Core::MessageItem::SignatureStateUnknown);
342  }
343 
344  mi->invalidateTagCache();
346 }
347 
349 {
350  Q_UNUSED(mi)
351  Akonadi::Item item = itemForRow(row);
352  item.setFlags(status.statusFlags());
353  auto job = new ItemModifyJob(item, this);
354  job->disableRevisionCheck();
355  job->setIgnorePayload(true);
356 }
357 
358 QVariant MessageList::StorageModel::data(const QModelIndex &index, int role) const
359 {
360  // We don't provide an implementation for data() in No-Akonadi-KMail.
361  // This is because StorageModel must be a wrapper anyway (because columns
362  // must be re-mapped and functions not available in a QAbstractItemModel
363  // are strictly needed. So when porting to Akonadi this class will
364  // either wrap or subclass the MessageModel and implement initializeMessageItem()
365  // with appropriate calls to data(). And for No-Akonadi-KMail we still have
366  // a somewhat efficient implementation.
367 
368  Q_UNUSED(index)
369  Q_UNUSED(role)
370 
371  return {};
372 }
373 
374 int MessageList::StorageModel::columnCount(const QModelIndex &parent) const
375 {
376  if (!parent.isValid()) {
377  return 1;
378  }
379  return 0; // this model is flat.
380 }
381 
382 QModelIndex MessageList::StorageModel::index(int row, int column, const QModelIndex &parent) const
383 {
384  if (!parent.isValid()) {
385  return createIndex(row, column, (void *)nullptr);
386  }
387  return {}; // this model is flat.
388 }
389 
391 {
392  Q_UNUSED(index)
393  return {}; // this model is flat.
394 }
395 
396 int MessageList::StorageModel::rowCount(const QModelIndex &parent) const
397 {
398  if (!parent.isValid()) {
399  return d->mModel->rowCount();
400  }
401  return 0; // this model is flat.
402 }
403 
405 {
406  auto data = new QMimeData();
407  QList<QUrl> urls;
408  urls.reserve(items.count());
409  for (MessageList::Core::MessageItem *mi : items) {
410  Akonadi::Item item = itemForRow(mi->currentModelIndexRow());
411  urls << item.url(Akonadi::Item::UrlWithMimeType);
412  }
413 
414  data->setUrls(urls);
415 
416  return data;
417 }
418 
419 void MessageList::StorageModel::StorageModelPrivate::onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
420 {
421  Q_EMIT q->dataChanged(q->index(topLeft.row(), 0), q->index(bottomRight.row(), 0));
422 }
423 
424 void MessageList::StorageModel::StorageModelPrivate::onSelectionChanged()
425 {
426  mFolderHash.clear();
427  Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount() - 1);
428 }
429 
430 void MessageList::StorageModel::StorageModelPrivate::loadSettings()
431 {
432  // Custom/System colors
433  MessageListSettings *settings = MessageListSettings::self();
434 
435  if (MessageCore::MessageCoreSettings::self()->useDefaultColors()) {
436  Core::MessageItem::setUnreadMessageColor(MessageList::Util::unreadDefaultMessageColor());
437  Core::MessageItem::setImportantMessageColor(MessageList::Util::importantDefaultMessageColor());
438  Core::MessageItem::setToDoMessageColor(MessageList::Util::todoDefaultMessageColor());
439  } else {
440  Core::MessageItem::setUnreadMessageColor(settings->unreadMessageColor());
441  Core::MessageItem::setImportantMessageColor(settings->importantMessageColor());
442  Core::MessageItem::setToDoMessageColor(settings->todoMessageColor());
443  }
444 
445  if (MessageCore::MessageCoreSettings::self()->useDefaultFonts()) {
446  Core::MessageItem::setGeneralFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
447  Core::MessageItem::setUnreadMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
448  Core::MessageItem::setImportantMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
449  Core::MessageItem::setToDoMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
450  } else {
451  Core::MessageItem::setGeneralFont(settings->messageListFont());
452  Core::MessageItem::setUnreadMessageFont(settings->unreadMessageFont());
453  Core::MessageItem::setImportantMessageFont(settings->importantMessageFont());
454  Core::MessageItem::setToDoMessageFont(settings->todoMessageFont());
455  }
456 }
457 
458 Akonadi::Item MessageList::StorageModel::itemForRow(int row) const
459 {
460  return d->mModel->data(d->mModel->index(row, 0), EntityTreeModel::ItemRole).value<Akonadi::Item>();
461 }
462 
463 KMime::Message::Ptr MessageList::StorageModel::messageForRow(int row) const
464 {
465  return messageForItem(itemForRow(row));
466 }
467 
468 Collection MessageList::StorageModel::parentCollectionForRow(int row) const
469 {
470  auto mimeProxy = static_cast<QAbstractProxyModel *>(d->mModel);
471  // This is index mapped to Akonadi::SelectionProxyModel
472  const QModelIndex childrenFilterIndex = mimeProxy->mapToSource(d->mModel->index(row, 0));
473  Q_ASSERT(childrenFilterIndex.isValid());
474 
475  auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
476  // This is index mapped to ETM
477  const QModelIndex etmIndex = childrenProxy->mapToSource(childrenFilterIndex);
478  Q_ASSERT(etmIndex.isValid());
479  // We cannot possibly refer to top-level collection
480  Q_ASSERT(etmIndex.parent().isValid());
481 
482  const auto col = etmIndex.parent().data(EntityTreeModel::CollectionRole).value<Collection>();
483  Q_ASSERT(col.isValid());
484 
485  return col;
486 }
487 
488 Akonadi::Collection MessageList::StorageModel::collectionForId(Akonadi::Collection::Id colId) const
489 {
490  // Get ETM
491  auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
492  if (childrenProxy) {
493  QAbstractItemModel *etm = childrenProxy->sourceModel();
494  if (etm) {
495  // get index in EntityTreeModel
496  const QModelIndex idx = EntityTreeModel::modelIndexForCollection(etm, Collection(colId));
497  if (idx.isValid()) {
498  // get and return collection
499  return idx.data(EntityTreeModel::CollectionRole).value<Collection>();
500  }
501  }
502  }
503  return {};
504 }
505 
506 void MessageList::StorageModel::resetModelStorage()
507 {
508  beginResetModel();
509  endResetModel();
510 }
511 
512 #include "moc_storagemodel.cpp"
void invalidateTagCache()
Deletes all cached tags.
The MessageItem class.
Definition: messageitem.h:34
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
QString number(int n, int base)
Flags flags() const
T value() const const
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
QString displayName() const
void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
QString trimmed() const const
bool testAndSetAcquire(T expectedValue, T newValue)
QFont systemFont(QFontDatabase::SystemFont type)
void chop(int n)
void setMessageItemStatus(MessageList::Core::MessageItem *mi, int row, Akonadi::MessageStatus status) override
This method should use the inner model implementation to associate the new status to the specified me...
int initialUnreadRowCountGuess() const override
Returns (a guess for) the number of unread messages: must be pessimistic (i.e.
bool containsOutboundMessages() const override
Returns true if this StorageModel (folder) contains outbound messages and false otherwise.
QByteArray trimmed() const const
QString id() const override
Returns an unique id for this Storage collection.
void setStatus(Akonadi::MessageStatus status)
Sets the status associated to this Item.
Definition: item.cpp:451
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
bool hasPayload() const
QMimeData * mimeData(const QVector< MessageList::Core::MessageItem * > &) const override
The implementation-specific mime data for this list of items.
void updateMessageItemData(MessageList::Core::MessageItem *mi, int row) const override
This method should use the inner model implementation to re-fill the date, the status,...
bool hasAttribute() const
StorageModel(QAbstractItemModel *model, QItemSelectionModel *selectionModel, QObject *parent=nullptr)
Create a StorageModel wrapping the specified folder.
QString stripOffPrefixes(const QString &subject)
Removes the forward and reply marks (e.g.
Definition: stringutil.cpp:783
qint64 size() const
void reserve(int alloc)
QString mimeType() const
QVariant data(int role) const const
QUrl url(UrlType type=UrlShort) const
void initialSetup(time_t date, size_t size, const QString &sender, const QString &receiver, bool useReceiver)
This is meant to be called right after the constructor.
Definition: item.cpp:551
Collection::Id storageCollectionId() const
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector< int > &roles)
Horizontal
bool isEmpty() const const
The Akonadi specific implementation of the Core::StorageModel.
Definition: storagemodel.h:35
const T * attribute() const
Q_SCRIPTABLE CaptureState status()
Collection & parentCollection()
void fillMessageItemThreadingData(MessageList::Core::MessageItem *mi, int row, ThreadingDataSubset subset) const override
This method should use the inner model implementation to fill in the specified subset of threading da...
void invalidateAnnotationCache()
Same as invalidateTagCache(), only for the annotation.
QString join(const QString &separator) const const
bool initializeMessageItem(MessageList::Core::MessageItem *mi, int row, bool bUseReceiver) const override
This method should use the inner model implementation to fill in the base data for the specified Mess...
bool isValid() const const
void reserve(int size)
virtual const QMetaObject * metaObject() const const
int row() const const
const char * className() const const
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
Id id() const
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsRemoved(const QModelIndex &parent, int first, int last)
CollectionStatistics statistics() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
int count(const T &value) const const
void layoutAboutToBeChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
QModelIndex parent() const const
bool isValid() const
void setFolder(const QString &folder)
Sets the folder associated to this Item.
Definition: item.cpp:546
void setFlags(const Flags &flags)
void clear()
QObject * parent() const const
void setSubject(const QString &subject)
Sets the subject associated to this Item.
Definition: item.cpp:536
QString remoteId() const
void modelAboutToBeReset()
void sort(Qt::CaseSensitivity cs)
int stat(const QString &path, KDE_struct_stat *buf)
T payload() const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Mar 26 2023 04:08:12 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.