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/KMime/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(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) {
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  Q_FALLTHROUGH();
297  const auto refs = mail->references()->identifiers();
298  if (!refs.isEmpty()) {
299  mi->setReferencesIdMD5(md5Encode(refs.last()));
300  }
301  }
302  Q_FALLTHROUGH();
303  // fall through
304  case PerfectThreadingOnly: {
305  mi->setMessageIdMD5(md5Encode(mail->messageID()->identifier()));
306  const auto inReplyTos = mail->inReplyTo()->identifiers();
307  if (!inReplyTos.isEmpty()) {
308  mi->setInReplyToIdMD5(md5Encode(inReplyTos.first()));
309  }
310  break;
311  }
312  default:
313  Q_ASSERT(false); // should never happen
314  break;
315  }
316 }
317 
319 {
320  const Akonadi::Item item = itemForRow(row);
321 
323  stat.setStatusFromFlags(item.flags());
324 
325  mi->setAkonadiItem(item);
326  mi->setStatus(stat);
327 
328  if (stat.isEncrypted()) {
329  mi->setEncryptionState(Core::MessageItem::FullyEncrypted);
330  } else {
331  mi->setEncryptionState(Core::MessageItem::EncryptionStateUnknown);
332  }
333 
334  if (stat.isSigned()) {
335  mi->setSignatureState(Core::MessageItem::FullySigned);
336  } else {
337  mi->setSignatureState(Core::MessageItem::SignatureStateUnknown);
338  }
339 
340  mi->invalidateTagCache();
342 }
343 
345 {
346  Q_UNUSED(mi)
347  Akonadi::Item item = itemForRow(row);
348  item.setFlags(status.statusFlags());
349  auto job = new ItemModifyJob(item, this);
350  job->disableRevisionCheck();
351  job->setIgnorePayload(true);
352 }
353 
354 QVariant MessageList::StorageModel::data(const QModelIndex &index, int role) const
355 {
356  // We don't provide an implementation for data() in No-Akonadi-KMail.
357  // This is because StorageModel must be a wrapper anyway (because columns
358  // must be re-mapped and functions not available in a QAbstractItemModel
359  // are strictly needed. So when porting to Akonadi this class will
360  // either wrap or subclass the MessageModel and implement initializeMessageItem()
361  // with appropriate calls to data(). And for No-Akonadi-KMail we still have
362  // a somewhat efficient implementation.
363 
364  Q_UNUSED(index)
365  Q_UNUSED(role)
366 
367  return {};
368 }
369 
370 int MessageList::StorageModel::columnCount(const QModelIndex &parent) const
371 {
372  if (!parent.isValid()) {
373  return 1;
374  }
375  return 0; // this model is flat.
376 }
377 
378 QModelIndex MessageList::StorageModel::index(int row, int column, const QModelIndex &parent) const
379 {
380  if (!parent.isValid()) {
381  return createIndex(row, column, (void *)nullptr);
382  }
383  return {}; // this model is flat.
384 }
385 
387 {
388  Q_UNUSED(index)
389  return {}; // this model is flat.
390 }
391 
392 int MessageList::StorageModel::rowCount(const QModelIndex &parent) const
393 {
394  if (!parent.isValid()) {
395  return d->mModel->rowCount();
396  }
397  return 0; // this model is flat.
398 }
399 
401 {
402  auto data = new QMimeData();
403  QList<QUrl> urls;
404  urls.reserve(items.count());
405  for (MessageList::Core::MessageItem *mi : items) {
406  Akonadi::Item item = itemForRow(mi->currentModelIndexRow());
407  urls << item.url(Akonadi::Item::UrlWithMimeType);
408  }
409 
410  data->setUrls(urls);
411 
412  return data;
413 }
414 
415 void MessageList::StorageModel::StorageModelPrivate::onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
416 {
417  Q_EMIT q->dataChanged(q->index(topLeft.row(), 0), q->index(bottomRight.row(), 0));
418 }
419 
420 void MessageList::StorageModel::StorageModelPrivate::onSelectionChanged()
421 {
422  mFolderHash.clear();
423  Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount() - 1);
424 }
425 
426 void MessageList::StorageModel::StorageModelPrivate::loadSettings()
427 {
428  // Custom/System colors
429  MessageListSettings *settings = MessageListSettings::self();
430 
431  if (MessageCore::MessageCoreSettings::self()->useDefaultColors()) {
432  Core::MessageItem::setUnreadMessageColor(MessageList::Util::unreadDefaultMessageColor());
433  Core::MessageItem::setImportantMessageColor(MessageList::Util::importantDefaultMessageColor());
434  Core::MessageItem::setToDoMessageColor(MessageList::Util::todoDefaultMessageColor());
435  } else {
436  Core::MessageItem::setUnreadMessageColor(settings->unreadMessageColor());
437  Core::MessageItem::setImportantMessageColor(settings->importantMessageColor());
438  Core::MessageItem::setToDoMessageColor(settings->todoMessageColor());
439  }
440 
441  if (MessageCore::MessageCoreSettings::self()->useDefaultFonts()) {
442  Core::MessageItem::setGeneralFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
443  Core::MessageItem::setUnreadMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
444  Core::MessageItem::setImportantMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
445  Core::MessageItem::setToDoMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
446  } else {
447  Core::MessageItem::setGeneralFont(settings->messageListFont());
448  Core::MessageItem::setUnreadMessageFont(settings->unreadMessageFont());
449  Core::MessageItem::setImportantMessageFont(settings->importantMessageFont());
450  Core::MessageItem::setToDoMessageFont(settings->todoMessageFont());
451  }
452 }
453 
454 Akonadi::Item MessageList::StorageModel::itemForRow(int row) const
455 {
456  return d->mModel->data(d->mModel->index(row, 0), EntityTreeModel::ItemRole).value<Akonadi::Item>();
457 }
458 
459 KMime::Message::Ptr MessageList::StorageModel::messageForRow(int row) const
460 {
461  return messageForItem(itemForRow(row));
462 }
463 
464 Collection MessageList::StorageModel::parentCollectionForRow(int row) const
465 {
466  auto mimeProxy = static_cast<QAbstractProxyModel *>(d->mModel);
467  // This is index mapped to Akonadi::SelectionProxyModel
468  const QModelIndex childrenFilterIndex = mimeProxy->mapToSource(d->mModel->index(row, 0));
469  Q_ASSERT(childrenFilterIndex.isValid());
470 
471  auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
472  // This is index mapped to ETM
473  const QModelIndex etmIndex = childrenProxy->mapToSource(childrenFilterIndex);
474  Q_ASSERT(etmIndex.isValid());
475  // We cannot possibly refer to top-level collection
476  Q_ASSERT(etmIndex.parent().isValid());
477 
478  const auto col = etmIndex.parent().data(EntityTreeModel::CollectionRole).value<Collection>();
479  Q_ASSERT(col.isValid());
480 
481  return col;
482 }
483 
484 Akonadi::Collection MessageList::StorageModel::collectionForId(Akonadi::Collection::Id colId) const
485 {
486  // Get ETM
487  auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
488  if (childrenProxy) {
489  QAbstractItemModel *etm = childrenProxy->sourceModel();
490  if (etm) {
491  // get index in EntityTreeModel
492  const QModelIndex idx = EntityTreeModel::modelIndexForCollection(etm, Collection(colId));
493  if (idx.isValid()) {
494  // get and return collection
495  return idx.data(EntityTreeModel::CollectionRole).value<Collection>();
496  }
497  }
498  }
499  return {};
500 }
501 
502 void MessageList::StorageModel::resetModelStorage()
503 {
504  beginResetModel();
505  endResetModel();
506 }
507 
508 #include "moc_storagemodel.cpp"
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
QUrl url(UrlType type=UrlShort) const
bool isValid() const
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
QString displayName() 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...
QByteArray trimmed() const const
The MessageItem class.
Definition: messageitem.h:34
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...
QObject * sender() const const
void reserve(int alloc)
bool isSigned() const
virtual const QMetaObject * metaObject() const const
T value() const const
QSet< QByteArray > statusFlags() const
void modelAboutToBeReset()
void setSubject(const QString &subject)
Sets the subject associated to this Item.
Definition: item.cpp:536
QString id() const override
Returns an unique id for this Storage collection.
qint64 size() const
QString join(const QString &separator) const const
QMimeData * mimeData(const QVector< MessageList::Core::MessageItem * > &) const override
The implementation-specific mime data for this list of items.
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void chop(int n)
T payload() const
Id id() const
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)
QString remoteId() const
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...)
Flags flags() const
StorageModel(QAbstractItemModel *model, QItemSelectionModel *selectionModel, QObject *parent=nullptr)
Create a StorageModel wrapping the specified folder.
bool isEmpty() const const
QString trimmed() const const
int row() const const
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:451
messageIdMD5, inReplyToMD5, referencesIdMD5
Attribute * attribute(const QByteArray &name)
void invalidateTagCache()
Deletes all cached tags.
QModelIndex createIndex(int row, int column, void *ptr) const const
bool containsOutboundMessages() const override
Returns true if this StorageModel (folder) contains outbound messages and false otherwise.
QString mimeType() const
int initialUnreadRowCountGuess() const override
Returns (a guess for) the number of unread messages: must be pessimistic (i.e.
void reserve(int size)
QString stripOffPrefixes(const QString &subject)
Removes the forward and reply marks (e.g.
Definition: stringutil.cpp:783
const char * className() const const
bool hasAttribute(const QByteArray &name) const
QFont systemFont(QFontDatabase::SystemFont type)
QVariant data(int role) const const
void setFolder(const QString &folder)
Sets the folder associated to this Item.
Definition: item.cpp:546
The Akonadi specific implementation of the Core::StorageModel.
Definition: storagemodel.h:35
int count(const T &value) const 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, 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.
QByteArray result() const const
void sort(Qt::CaseSensitivity cs)
Collection::Id storageCollectionId() const
Horizontal
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...
bool hasPayload() const
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
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:551
Q_EMITQ_EMIT
void setFlags(const Flags &flags)
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 Fri Nov 26 2021 23:16:43 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.