Messagelib

storagemodel.cpp
1/*
2 SPDX-FileCopyrightText: 2009 Kevin Ottens <ervin@kde.org>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "storagemodel.h"
8
9#include <MessageCore/MessageCoreSettings>
10#include <MessageCore/StringUtil>
11
12#include <Akonadi/AttributeFactory>
13#include <Akonadi/CollectionStatistics>
14#include <Akonadi/EntityMimeTypeFilterModel>
15#include <Akonadi/EntityTreeModel>
16#include <Akonadi/ItemModifyJob>
17#include <Akonadi/MessageFolderAttribute>
18#include <Akonadi/SelectionProxyModel>
19
20#include "core/md5hash.h"
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
36namespace MessageList
37{
38class Q_DECL_HIDDEN StorageModel::StorageModelPrivate
39{
40public:
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
60using namespace Akonadi;
61using namespace MessageList;
62
63namespace
64{
65KMime::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
75static 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
126MessageList::StorageModel::~StorageModel() = default;
127
128Collection::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
161bool 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 (auto from = mail->from(false)) {
211 sender = from->asUnicodeString();
212 }
213 QString receiver;
214 if (auto to = mail->to(false)) {
215 receiver = 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;
234 if (auto subjectMail = mail->subject(false)) {
235 subject = subjectMail->asUnicodeString();
236 if (subject.isEmpty()) {
237 subject = QLatin1Char('(') + noSubject + QLatin1Char(')');
238 }
239 }
240
241 mi->setSubject(subject);
242
243 auto it = d->mFolderHash.find(item.storageCollectionId());
244 if (it == d->mFolderHash.end()) {
245 QString folder;
246 Collection collection = collectionForId(item.storageCollectionId());
247 while (collection.parentCollection().isValid()) {
248 folder = collection.displayName() + QLatin1Char('/') + folder;
249 collection = collection.parentCollection();
250 }
251 folder.chop(1);
252 it = d->mFolderHash.insert(item.storageCollectionId(), folder);
253 }
254 mi->setFolder(it.value());
255
256 updateMessageItemData(mi, row);
257
258 return true;
259}
260
261static Core::MD5Hash md5Encode(QByteArrayView str)
262{
263 auto trimmed = str.trimmed();
264 if (trimmed.isEmpty()) {
265 return {};
266 }
267
268 static thread_local QCryptographicHash c(QCryptographicHash::Md5);
269 c.reset();
270 c.addData(trimmed);
271 return c.resultView();
272}
273
274static Core::MD5Hash md5Encode(QStringView str)
275{
276 auto trimmed = str.trimmed();
277 if (trimmed.isEmpty()) {
278 return {};
279 }
280
281 static thread_local QCryptographicHash c(QCryptographicHash::Md5);
282 c.reset();
283 c.addData(QByteArrayView(reinterpret_cast<const char *>(trimmed.utf16()), sizeof(QStringView::storage_type) * trimmed.length()));
284 return c.resultView();
285}
286
288{
289 const KMime::Message::Ptr mail = messageForRow(row);
290 Q_ASSERT(mail); // We ASSUME that initializeMessageItem has been called successfully...
291
292 switch (subset) {
293 case PerfectThreadingReferencesAndSubject: {
294 const QString subject = mail->subject()->asUnicodeString();
295 const QString strippedSubject = MessageCore::StringUtil::stripOffPrefixes(subject);
296 mi->setStrippedSubjectMD5(md5Encode(strippedSubject));
297 mi->setSubjectIsPrefixed(subject != strippedSubject);
298 // fall through
299 }
300 [[fallthrough]];
301 case PerfectThreadingPlusReferences: {
302 const auto refs = mail->references()->identifiers();
303 if (!refs.isEmpty()) {
304 mi->setReferencesIdMD5(md5Encode(refs.last()));
305 }
306 }
307 [[fallthrough]];
308 // fall through
309 case PerfectThreadingOnly: {
310 mi->setMessageIdMD5(md5Encode(mail->messageID()->identifier()));
311 const auto inReplyTos = mail->inReplyTo()->identifiers();
312 if (auto inReplyTos = mail->inReplyTo()) {
313 if (!inReplyTos->identifiers().isEmpty()) {
314 mi->setInReplyToIdMD5(md5Encode(inReplyTos->identifiers().constFirst()));
315 }
316 }
317 break;
318 }
319 default:
320 Q_ASSERT(false); // should never happen
321 break;
322 }
323}
324
326{
327 const Akonadi::Item item = itemForRow(row);
328
330 stat.setStatusFromFlags(item.flags());
331
332 mi->setAkonadiItem(item);
333 mi->setStatus(stat);
334
335 if (stat.isEncrypted()) {
336 mi->setEncryptionState(Core::MessageItem::FullyEncrypted);
337 } else {
338 mi->setEncryptionState(Core::MessageItem::EncryptionStateUnknown);
339 }
340
341 if (stat.isSigned()) {
342 mi->setSignatureState(Core::MessageItem::FullySigned);
343 } else {
344 mi->setSignatureState(Core::MessageItem::SignatureStateUnknown);
345 }
346
347 mi->invalidateTagCache();
348}
349
351{
352 Q_UNUSED(mi)
353 Akonadi::Item item = itemForRow(row);
354 item.setFlags(status.statusFlags());
355 auto job = new ItemModifyJob(item, this);
356 job->disableRevisionCheck();
357 job->setIgnorePayload(true);
358}
359
360QVariant MessageList::StorageModel::data(const QModelIndex &index, int role) const
361{
362 // We don't provide an implementation for data() in No-Akonadi-KMail.
363 // This is because StorageModel must be a wrapper anyway (because columns
364 // must be re-mapped and functions not available in a QAbstractItemModel
365 // are strictly needed. So when porting to Akonadi this class will
366 // either wrap or subclass the MessageModel and implement initializeMessageItem()
367 // with appropriate calls to data(). And for No-Akonadi-KMail we still have
368 // a somewhat efficient implementation.
369
370 Q_UNUSED(index)
371 Q_UNUSED(role)
372
373 return {};
374}
375
376int MessageList::StorageModel::columnCount(const QModelIndex &parent) const
377{
378 if (!parent.isValid()) {
379 return 1;
380 }
381 return 0; // this model is flat.
382}
383
384QModelIndex MessageList::StorageModel::index(int row, int column, const QModelIndex &parent) const
385{
386 if (!parent.isValid()) {
387 return createIndex(row, column, (void *)nullptr);
388 }
389 return {}; // this model is flat.
390}
391
393{
394 Q_UNUSED(index)
395 return {}; // this model is flat.
396}
397
398int MessageList::StorageModel::rowCount(const QModelIndex &parent) const
399{
400 if (!parent.isValid()) {
401 return d->mModel->rowCount();
402 }
403 return 0; // this model is flat.
404}
405
407{
408 auto data = new QMimeData();
409 QList<QUrl> urls;
410 urls.reserve(items.count());
411 for (MessageList::Core::MessageItem *mi : items) {
412 Akonadi::Item item = itemForRow(mi->currentModelIndexRow());
414 }
415
416 data->setUrls(urls);
417
418 return data;
419}
420
421void MessageList::StorageModel::StorageModelPrivate::onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
422{
423 Q_EMIT q->dataChanged(q->index(topLeft.row(), 0), q->index(bottomRight.row(), 0));
424}
425
426void MessageList::StorageModel::StorageModelPrivate::onSelectionChanged()
427{
428 mFolderHash.clear();
429 Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount() - 1);
430}
431
432void MessageList::StorageModel::StorageModelPrivate::loadSettings()
433{
434 // Custom/System colors
435 MessageListSettings *settings = MessageListSettings::self();
436
437 if (MessageCore::MessageCoreSettings::self()->useDefaultColors()) {
438 Core::MessageItem::setUnreadMessageColor(MessageList::Util::unreadDefaultMessageColor());
439 Core::MessageItem::setImportantMessageColor(MessageList::Util::importantDefaultMessageColor());
440 Core::MessageItem::setToDoMessageColor(MessageList::Util::todoDefaultMessageColor());
441 } else {
442 Core::MessageItem::setUnreadMessageColor(settings->unreadMessageColor());
443 Core::MessageItem::setImportantMessageColor(settings->importantMessageColor());
444 Core::MessageItem::setToDoMessageColor(settings->todoMessageColor());
445 }
446
447 if (MessageCore::MessageCoreSettings::self()->useDefaultFonts()) {
448 Core::MessageItem::setGeneralFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
449 Core::MessageItem::setUnreadMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
450 Core::MessageItem::setImportantMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
451 Core::MessageItem::setToDoMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
452 } else {
453 Core::MessageItem::setGeneralFont(settings->messageListFont());
454 Core::MessageItem::setUnreadMessageFont(settings->unreadMessageFont());
455 Core::MessageItem::setImportantMessageFont(settings->importantMessageFont());
456 Core::MessageItem::setToDoMessageFont(settings->todoMessageFont());
457 }
458}
459
460Akonadi::Item MessageList::StorageModel::itemForRow(int row) const
461{
462 return d->mModel->data(d->mModel->index(row, 0), EntityTreeModel::ItemRole).value<Akonadi::Item>();
463}
464
465KMime::Message::Ptr MessageList::StorageModel::messageForRow(int row) const
466{
467 return messageForItem(itemForRow(row));
468}
469
470Collection MessageList::StorageModel::parentCollectionForRow(int row) const
471{
472 auto mimeProxy = static_cast<QAbstractProxyModel *>(d->mModel);
473 // This is index mapped to Akonadi::SelectionProxyModel
474 const QModelIndex childrenFilterIndex = mimeProxy->mapToSource(d->mModel->index(row, 0));
475 Q_ASSERT(childrenFilterIndex.isValid());
476
477 auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
478 // This is index mapped to ETM
479 const QModelIndex etmIndex = childrenProxy->mapToSource(childrenFilterIndex);
480 Q_ASSERT(etmIndex.isValid());
481 // We cannot possibly refer to top-level collection
482 Q_ASSERT(etmIndex.parent().isValid());
483
484 const auto col = etmIndex.parent().data(EntityTreeModel::CollectionRole).value<Collection>();
485 Q_ASSERT(col.isValid());
486
487 return col;
488}
489
490Akonadi::Collection MessageList::StorageModel::collectionForId(Akonadi::Collection::Id colId) const
491{
492 // Get ETM
493 auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
494 if (childrenProxy) {
495 QAbstractItemModel *etm = childrenProxy->sourceModel();
496 if (etm) {
497 // get index in EntityTreeModel
499 if (idx.isValid()) {
500 // get and return collection
502 }
503 }
504 }
505 return {};
506}
507
508void MessageList::StorageModel::resetModelStorage()
509{
510 beginResetModel();
511 endResetModel();
512}
513
514#include "moc_storagemodel.cpp"
static QString mimeType()
CollectionStatistics statistics() const
bool isValid() const
QString displayName() const
const T * attribute() const
Collection & parentCollection()
bool hasAttribute() const
static QModelIndex modelIndexForCollection(const QAbstractItemModel *model, const Collection &collection)
qint64 size() const
QString mimeType() const
T payload() const
QUrl url(UrlType type=UrlShort) const
Flags flags() const
Id id() const
Collection::Id storageCollectionId() const
QString remoteId() const
void setFlags(const Flags &flags)
bool hasPayload() 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
void setStatus(Akonadi::MessageStatus status)
Sets the status associated to this Item.
Definition item.cpp:451
void setSubject(const QString &subject)
Sets the subject associated to this Item.
Definition item.cpp:536
void setFolder(const QString &folder)
Sets the folder associated to this Item.
Definition item.cpp:546
Compact storage of the result of an MD5 hash computation, for use in the threading code.
Definition md5hash.h:18
The MessageItem class.
Definition messageitem.h:36
void invalidateTagCache()
Deletes all cached tags.
The Akonadi specific implementation of the Core::StorageModel.
int initialUnreadRowCountGuess() const override
Returns (a guess for) the number of unread messages: must be pessimistic (i.e.
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 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 containsOutboundMessages() const override
Returns true if this StorageModel (folder) contains outbound messages and false otherwise.
StorageModel(QAbstractItemModel *model, QItemSelectionModel *selectionModel, QObject *parent=nullptr)
Create a StorageModel wrapping the specified folder.
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,...
QString id() const override
Returns an unique id for this Storage collection.
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...
QMimeData * mimeData(const QList< MessageList::Core::MessageItem * > &) const override
The implementation-specific mime data for this list of items.
Q_SCRIPTABLE CaptureState status()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString stripOffPrefixes(const QString &subject)
Removes the forward and reply marks (e.g.
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
void layoutAboutToBeChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
void modelAboutToBeReset()
void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsRemoved(const QModelIndex &parent, int first, int last)
bool testAndSetAcquire(T expectedValue, T newValue)
QByteArrayView trimmed() const const
QFont systemFont(SystemFont type)
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
qsizetype count() const const
void reserve(qsizetype size)
const char * className() const const
QVariant data(int role) const const
bool isValid() const const
QModelIndex parent() const const
int row() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual const QMetaObject * metaObject() const const
QObject * parent() const const
virtual void setSourceModel(QAbstractItemModel *sourceModel) override
void chop(qsizetype n)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString join(QChar separator) const const
void sort(Qt::CaseSensitivity cs)
QStringView trimmed() const const
Horizontal
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:07:25 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.