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  c.addData(QByteArrayView(reinterpret_cast<const char *>(trimmed.unicode()), sizeof(QChar) * trimmed.length()));
279  return c.result();
280 }
281 
283 {
284  const KMime::Message::Ptr mail = messageForRow(row);
285  Q_ASSERT(mail); // We ASSUME that initializeMessageItem has been called successfully...
286 
287  switch (subset) {
288  case PerfectThreadingReferencesAndSubject: {
289  const QString subject = mail->subject()->asUnicodeString();
290  const QString strippedSubject = MessageCore::StringUtil::stripOffPrefixes(subject);
291  mi->setStrippedSubjectMD5(md5Encode(strippedSubject));
292  mi->setSubjectIsPrefixed(subject != strippedSubject);
293  // fall through
294  }
295  [[fallthrough]];
296  case PerfectThreadingPlusReferences: {
297  const auto refs = mail->references()->identifiers();
298  if (!refs.isEmpty()) {
299  mi->setReferencesIdMD5(md5Encode(refs.last()));
300  }
301  }
302  [[fallthrough]];
303  // fall through
304  case PerfectThreadingOnly: {
305  mi->setMessageIdMD5(md5Encode(mail->messageID()->identifier()));
306  const auto inReplyTos = mail->inReplyTo()->identifiers();
307  if (auto inReplyTos = mail->inReplyTo()) {
308  if (!inReplyTos->identifiers().isEmpty()) {
309  mi->setInReplyToIdMD5(md5Encode(inReplyTos->identifiers().constFirst()));
310  }
311  }
312  break;
313  }
314  default:
315  Q_ASSERT(false); // should never happen
316  break;
317  }
318 }
319 
321 {
322  const Akonadi::Item item = itemForRow(row);
323 
325  stat.setStatusFromFlags(item.flags());
326 
327  mi->setAkonadiItem(item);
328  mi->setStatus(stat);
329 
330  if (stat.isEncrypted()) {
331  mi->setEncryptionState(Core::MessageItem::FullyEncrypted);
332  } else {
333  mi->setEncryptionState(Core::MessageItem::EncryptionStateUnknown);
334  }
335 
336  if (stat.isSigned()) {
337  mi->setSignatureState(Core::MessageItem::FullySigned);
338  } else {
339  mi->setSignatureState(Core::MessageItem::SignatureStateUnknown);
340  }
341 
342  mi->invalidateTagCache();
344 }
345 
347 {
348  Q_UNUSED(mi)
349  Akonadi::Item item = itemForRow(row);
350  item.setFlags(status.statusFlags());
351  auto job = new ItemModifyJob(item, this);
352  job->disableRevisionCheck();
353  job->setIgnorePayload(true);
354 }
355 
356 QVariant MessageList::StorageModel::data(const QModelIndex &index, int role) const
357 {
358  // We don't provide an implementation for data() in No-Akonadi-KMail.
359  // This is because StorageModel must be a wrapper anyway (because columns
360  // must be re-mapped and functions not available in a QAbstractItemModel
361  // are strictly needed. So when porting to Akonadi this class will
362  // either wrap or subclass the MessageModel and implement initializeMessageItem()
363  // with appropriate calls to data(). And for No-Akonadi-KMail we still have
364  // a somewhat efficient implementation.
365 
366  Q_UNUSED(index)
367  Q_UNUSED(role)
368 
369  return {};
370 }
371 
372 int MessageList::StorageModel::columnCount(const QModelIndex &parent) const
373 {
374  if (!parent.isValid()) {
375  return 1;
376  }
377  return 0; // this model is flat.
378 }
379 
380 QModelIndex MessageList::StorageModel::index(int row, int column, const QModelIndex &parent) const
381 {
382  if (!parent.isValid()) {
383  return createIndex(row, column, (void *)nullptr);
384  }
385  return {}; // this model is flat.
386 }
387 
389 {
390  Q_UNUSED(index)
391  return {}; // this model is flat.
392 }
393 
394 int MessageList::StorageModel::rowCount(const QModelIndex &parent) const
395 {
396  if (!parent.isValid()) {
397  return d->mModel->rowCount();
398  }
399  return 0; // this model is flat.
400 }
401 
403 {
404  auto data = new QMimeData();
405  QList<QUrl> urls;
406  urls.reserve(items.count());
407  for (MessageList::Core::MessageItem *mi : items) {
408  Akonadi::Item item = itemForRow(mi->currentModelIndexRow());
409  urls << item.url(Akonadi::Item::UrlWithMimeType);
410  }
411 
412  data->setUrls(urls);
413 
414  return data;
415 }
416 
417 void MessageList::StorageModel::StorageModelPrivate::onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
418 {
419  Q_EMIT q->dataChanged(q->index(topLeft.row(), 0), q->index(bottomRight.row(), 0));
420 }
421 
422 void MessageList::StorageModel::StorageModelPrivate::onSelectionChanged()
423 {
424  mFolderHash.clear();
425  Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount() - 1);
426 }
427 
428 void MessageList::StorageModel::StorageModelPrivate::loadSettings()
429 {
430  // Custom/System colors
431  MessageListSettings *settings = MessageListSettings::self();
432 
433  if (MessageCore::MessageCoreSettings::self()->useDefaultColors()) {
434  Core::MessageItem::setUnreadMessageColor(MessageList::Util::unreadDefaultMessageColor());
435  Core::MessageItem::setImportantMessageColor(MessageList::Util::importantDefaultMessageColor());
436  Core::MessageItem::setToDoMessageColor(MessageList::Util::todoDefaultMessageColor());
437  } else {
438  Core::MessageItem::setUnreadMessageColor(settings->unreadMessageColor());
439  Core::MessageItem::setImportantMessageColor(settings->importantMessageColor());
440  Core::MessageItem::setToDoMessageColor(settings->todoMessageColor());
441  }
442 
443  if (MessageCore::MessageCoreSettings::self()->useDefaultFonts()) {
444  Core::MessageItem::setGeneralFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
445  Core::MessageItem::setUnreadMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
446  Core::MessageItem::setImportantMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
447  Core::MessageItem::setToDoMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
448  } else {
449  Core::MessageItem::setGeneralFont(settings->messageListFont());
450  Core::MessageItem::setUnreadMessageFont(settings->unreadMessageFont());
451  Core::MessageItem::setImportantMessageFont(settings->importantMessageFont());
452  Core::MessageItem::setToDoMessageFont(settings->todoMessageFont());
453  }
454 }
455 
456 Akonadi::Item MessageList::StorageModel::itemForRow(int row) const
457 {
458  return d->mModel->data(d->mModel->index(row, 0), EntityTreeModel::ItemRole).value<Akonadi::Item>();
459 }
460 
461 KMime::Message::Ptr MessageList::StorageModel::messageForRow(int row) const
462 {
463  return messageForItem(itemForRow(row));
464 }
465 
466 Collection MessageList::StorageModel::parentCollectionForRow(int row) const
467 {
468  auto mimeProxy = static_cast<QAbstractProxyModel *>(d->mModel);
469  // This is index mapped to Akonadi::SelectionProxyModel
470  const QModelIndex childrenFilterIndex = mimeProxy->mapToSource(d->mModel->index(row, 0));
471  Q_ASSERT(childrenFilterIndex.isValid());
472 
473  auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
474  // This is index mapped to ETM
475  const QModelIndex etmIndex = childrenProxy->mapToSource(childrenFilterIndex);
476  Q_ASSERT(etmIndex.isValid());
477  // We cannot possibly refer to top-level collection
478  Q_ASSERT(etmIndex.parent().isValid());
479 
480  const auto col = etmIndex.parent().data(EntityTreeModel::CollectionRole).value<Collection>();
481  Q_ASSERT(col.isValid());
482 
483  return col;
484 }
485 
486 Akonadi::Collection MessageList::StorageModel::collectionForId(Akonadi::Collection::Id colId) const
487 {
488  // Get ETM
489  auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
490  if (childrenProxy) {
491  QAbstractItemModel *etm = childrenProxy->sourceModel();
492  if (etm) {
493  // get index in EntityTreeModel
494  const QModelIndex idx = EntityTreeModel::modelIndexForCollection(etm, Collection(colId));
495  if (idx.isValid()) {
496  // get and return collection
497  return idx.data(EntityTreeModel::CollectionRole).value<Collection>();
498  }
499  }
500  }
501  return {};
502 }
503 
504 void MessageList::StorageModel::resetModelStorage()
505 {
506  beginResetModel();
507  endResetModel();
508 }
509 
510 #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
int count(const T &value) const 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
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
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...)
void layoutAboutToBeChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
QModelIndex parent() const const
bool isValid() const
QMimeData * mimeData(const QList< MessageList::Core::MessageItem * > &) const override
The implementation-specific mime data for this list of items.
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 Tue Nov 28 2023 04:03:07 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.