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

KDE's Doxygen guidelines are available online.