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/messageitem.h"
21#include "messagelist_debug.h"
22#include "messagelistsettings.h"
23#include "messagelistutil.h"
24#include <KLocalizedString>
25#include <QUrl>
26
27#include <QAbstractItemModel>
28#include <QAtomicInt>
29#include <QCryptographicHash>
30#include <QFontDatabase>
31#include <QHash>
32#include <QItemSelectionModel>
33#include <QMimeData>
34
35namespace MessageList
36{
37class Q_DECL_HIDDEN StorageModel::StorageModelPrivate
38{
39public:
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 StorageModelPrivate(StorageModel *owner)
53 : q(owner)
54 {
55 }
56};
57} // namespace MessageList
58
59using namespace Akonadi;
60using namespace MessageList;
61
62namespace
63{
64KMime::Message::Ptr messageForItem(const Akonadi::Item &item)
65{
66 if (!item.hasPayload<KMime::Message::Ptr>()) {
67 qCWarning(MESSAGELIST_LOG) << "Not a message" << item.id() << item.remoteId() << item.mimeType();
68 return {};
69 }
70 return item.payload<KMime::Message::Ptr>();
71}
72}
73
74static QAtomicInt _k_attributeInitialized;
75
77 : Core::StorageModel(parent)
78 , d(new StorageModelPrivate(this))
79{
80 d->mSelectionModel = selectionModel;
81 if (_k_attributeInitialized.testAndSetAcquire(0, 1)) {
83 }
84
85 auto childrenFilter = new Akonadi::SelectionProxyModel(d->mSelectionModel, this);
86 childrenFilter->setSourceModel(model);
87 childrenFilter->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection);
88 d->mChildrenFilterModel = childrenFilter;
89
90 auto itemFilter = new EntityMimeTypeFilterModel(this);
91 itemFilter->setSourceModel(childrenFilter);
92 itemFilter->addMimeTypeExclusionFilter(Collection::mimeType());
93 itemFilter->addMimeTypeInclusionFilter(QStringLiteral("message/rfc822"));
94 itemFilter->setHeaderGroup(EntityTreeModel::ItemListHeaders);
95
96 d->mModel = itemFilter;
97
98 qCDebug(MESSAGELIST_LOG) << "Using model:" << model->metaObject()->className();
99
100 connect(d->mModel, &QAbstractItemModel::dataChanged, 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, this, [this]() {
116 d->onSelectionChanged();
117 });
118
119 d->loadSettings();
120 connect(MessageListSettings::self(), &MessageListSettings::configChanged, this, [this]() {
121 d->loadSettings();
122 });
123}
124
125MessageList::StorageModel::~StorageModel() = default;
126
127Collection::List MessageList::StorageModel::displayedCollections() const
128{
129 Collection::List collections;
130 const QModelIndexList indexes = d->mSelectionModel->selectedRows();
131
132 collections.reserve(indexes.count());
133 for (const QModelIndex &index : indexes) {
134 auto c = index.data(EntityTreeModel::CollectionRole).value<Collection>();
135 if (c.isValid()) {
136 collections << c;
137 }
138 }
139
140 return collections;
141}
142
144{
145 QStringList ids;
146 const QModelIndexList indexes = d->mSelectionModel->selectedRows();
147
148 ids.reserve(indexes.count());
149 for (const QModelIndex &index : indexes) {
150 auto c = index.data(EntityTreeModel::CollectionRole).value<Collection>();
151 if (c.isValid()) {
152 ids << QString::number(c.id());
153 }
154 }
155
156 ids.sort();
157 return ids.join(QLatin1Char(':'));
158}
159
160bool MessageList::StorageModel::isOutBoundFolder(const Akonadi::Collection &c) const
161{
163 return true;
164 }
165 return false;
166}
167
169{
170 const QModelIndexList indexes = d->mSelectionModel->selectedRows();
171
172 for (const QModelIndex &index : indexes) {
173 auto c = index.data(EntityTreeModel::CollectionRole).value<Collection>();
174 if (c.isValid()) {
175 return isOutBoundFolder(c);
176 }
177 }
178
179 return false;
180}
181
183{
184 const QModelIndexList indexes = d->mSelectionModel->selectedRows();
185
186 int unreadCount = 0;
187
188 for (const QModelIndex &index : indexes) {
189 auto c = index.data(EntityTreeModel::CollectionRole).value<Collection>();
190 if (c.isValid()) {
191 unreadCount += c.statistics().unreadCount();
192 }
193 }
194
195 return unreadCount;
196}
197
199{
200 const Akonadi::Item item = itemForRow(row);
201 const KMime::Message::Ptr mail = messageForItem(item);
202 if (!mail) {
203 return false;
204 }
205
206 const Collection parentCol = parentCollectionForRow(row);
207
208 QString sender;
209 if (auto from = mail->from(false)) {
210 sender = from->asUnicodeString();
211 }
212 QString receiver;
213 if (auto to = mail->to(false)) {
214 receiver = to->asUnicodeString();
215 }
216
217 // Static for speed reasons
218 static const QString noSubject = i18nc("displayed as subject when the subject of a mail is empty", "No Subject");
219 static const QString unknown(i18nc("displayed when a mail has unknown sender, receiver or date", "Unknown"));
220
221 if (sender.isEmpty()) {
222 sender = unknown;
223 }
224 if (receiver.isEmpty()) {
225 receiver = unknown;
226 }
227
228 mi->initialSetup(mail->date()->dateTime().toSecsSinceEpoch(), item.size(), sender, receiver, bUseReceiver);
229 mi->setItemId(item.id());
230 mi->setParentCollectionId(parentCol.id());
231
232 QString subject;
233 if (auto subjectMail = mail->subject(false)) {
234 subject = subjectMail->asUnicodeString();
235 if (subject.isEmpty()) {
236 subject = QLatin1Char('(') + noSubject + QLatin1Char(')');
237 }
238 }
239
240 mi->setSubject(subject);
241
242 auto it = d->mFolderHash.find(item.storageCollectionId());
243 if (it == d->mFolderHash.end()) {
244 QString folder;
245 Collection collection = collectionForId(item.storageCollectionId());
246 while (collection.parentCollection().isValid()) {
247 folder = collection.displayName() + QLatin1Char('/') + folder;
248 collection = collection.parentCollection();
249 }
250 folder.chop(1);
251 it = d->mFolderHash.insert(item.storageCollectionId(), folder);
252 }
253 mi->setFolder(it.value());
254
255 updateMessageItemData(mi, row);
256
257 return true;
258}
259
260static QByteArray md5Encode(const QByteArray &str)
261{
262 auto trimmed = str.trimmed();
263 if (trimmed.isEmpty()) {
264 return {};
265 }
266
268 c.addData(trimmed);
269 return c.result();
270}
271
272static QByteArray md5Encode(const QString &str)
273{
274 auto trimmed = str.trimmed();
275 if (trimmed.isEmpty()) {
276 return {};
277 }
278
280 c.addData(QByteArrayView(reinterpret_cast<const char *>(trimmed.unicode()), sizeof(QChar) * trimmed.length()));
281 return c.result();
282}
283
285{
286 const KMime::Message::Ptr mail = messageForRow(row);
287 Q_ASSERT(mail); // We ASSUME that initializeMessageItem has been called successfully...
288
289 switch (subset) {
290 case PerfectThreadingReferencesAndSubject: {
291 const QString subject = mail->subject()->asUnicodeString();
292 const QString strippedSubject = MessageCore::StringUtil::stripOffPrefixes(subject);
293 mi->setStrippedSubjectMD5(md5Encode(strippedSubject));
294 mi->setSubjectIsPrefixed(subject != strippedSubject);
295 // fall through
296 }
297 [[fallthrough]];
298 case PerfectThreadingPlusReferences: {
299 const auto refs = mail->references()->identifiers();
300 if (!refs.isEmpty()) {
301 mi->setReferencesIdMD5(md5Encode(refs.last()));
302 }
303 }
304 [[fallthrough]];
305 // fall through
306 case PerfectThreadingOnly: {
307 mi->setMessageIdMD5(md5Encode(mail->messageID()->identifier()));
308 const auto inReplyTos = mail->inReplyTo()->identifiers();
309 if (auto inReplyTos = mail->inReplyTo()) {
310 if (!inReplyTos->identifiers().isEmpty()) {
311 mi->setInReplyToIdMD5(md5Encode(inReplyTos->identifiers().constFirst()));
312 }
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();
345}
346
348{
349 Q_UNUSED(mi)
350 Akonadi::Item item = itemForRow(row);
351 item.setFlags(status.statusFlags());
352 auto job = new ItemModifyJob(item, this);
353 job->disableRevisionCheck();
354 job->setIgnorePayload(true);
355}
356
357QVariant MessageList::StorageModel::data(const QModelIndex &index, int role) const
358{
359 // We don't provide an implementation for data() in No-Akonadi-KMail.
360 // This is because StorageModel must be a wrapper anyway (because columns
361 // must be re-mapped and functions not available in a QAbstractItemModel
362 // are strictly needed. So when porting to Akonadi this class will
363 // either wrap or subclass the MessageModel and implement initializeMessageItem()
364 // with appropriate calls to data(). And for No-Akonadi-KMail we still have
365 // a somewhat efficient implementation.
366
367 Q_UNUSED(index)
368 Q_UNUSED(role)
369
370 return {};
371}
372
373int MessageList::StorageModel::columnCount(const QModelIndex &parent) const
374{
375 if (!parent.isValid()) {
376 return 1;
377 }
378 return 0; // this model is flat.
379}
380
381QModelIndex MessageList::StorageModel::index(int row, int column, const QModelIndex &parent) const
382{
383 if (!parent.isValid()) {
384 return createIndex(row, column, (void *)nullptr);
385 }
386 return {}; // this model is flat.
387}
388
390{
391 Q_UNUSED(index)
392 return {}; // this model is flat.
393}
394
395int MessageList::StorageModel::rowCount(const QModelIndex &parent) const
396{
397 if (!parent.isValid()) {
398 return d->mModel->rowCount();
399 }
400 return 0; // this model is flat.
401}
402
404{
405 auto data = new QMimeData();
406 QList<QUrl> urls;
407 urls.reserve(items.count());
408 for (MessageList::Core::MessageItem *mi : items) {
409 Akonadi::Item item = itemForRow(mi->currentModelIndexRow());
411 }
412
413 data->setUrls(urls);
414
415 return data;
416}
417
418void MessageList::StorageModel::StorageModelPrivate::onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
419{
420 Q_EMIT q->dataChanged(q->index(topLeft.row(), 0), q->index(bottomRight.row(), 0));
421}
422
423void MessageList::StorageModel::StorageModelPrivate::onSelectionChanged()
424{
425 mFolderHash.clear();
426 Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount() - 1);
427}
428
429void MessageList::StorageModel::StorageModelPrivate::loadSettings()
430{
431 // Custom/System colors
432 MessageListSettings *settings = MessageListSettings::self();
433
434 if (MessageCore::MessageCoreSettings::self()->useDefaultColors()) {
435 Core::MessageItem::setUnreadMessageColor(MessageList::Util::unreadDefaultMessageColor());
436 Core::MessageItem::setImportantMessageColor(MessageList::Util::importantDefaultMessageColor());
437 Core::MessageItem::setToDoMessageColor(MessageList::Util::todoDefaultMessageColor());
438 } else {
439 Core::MessageItem::setUnreadMessageColor(settings->unreadMessageColor());
440 Core::MessageItem::setImportantMessageColor(settings->importantMessageColor());
441 Core::MessageItem::setToDoMessageColor(settings->todoMessageColor());
442 }
443
444 if (MessageCore::MessageCoreSettings::self()->useDefaultFonts()) {
445 Core::MessageItem::setGeneralFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
446 Core::MessageItem::setUnreadMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
447 Core::MessageItem::setImportantMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
448 Core::MessageItem::setToDoMessageFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
449 } else {
450 Core::MessageItem::setGeneralFont(settings->messageListFont());
451 Core::MessageItem::setUnreadMessageFont(settings->unreadMessageFont());
452 Core::MessageItem::setImportantMessageFont(settings->importantMessageFont());
453 Core::MessageItem::setToDoMessageFont(settings->todoMessageFont());
454 }
455}
456
457Akonadi::Item MessageList::StorageModel::itemForRow(int row) const
458{
459 return d->mModel->data(d->mModel->index(row, 0), EntityTreeModel::ItemRole).value<Akonadi::Item>();
460}
461
462KMime::Message::Ptr MessageList::StorageModel::messageForRow(int row) const
463{
464 return messageForItem(itemForRow(row));
465}
466
467Collection MessageList::StorageModel::parentCollectionForRow(int row) const
468{
469 auto mimeProxy = static_cast<QAbstractProxyModel *>(d->mModel);
470 // This is index mapped to Akonadi::SelectionProxyModel
471 const QModelIndex childrenFilterIndex = mimeProxy->mapToSource(d->mModel->index(row, 0));
472 Q_ASSERT(childrenFilterIndex.isValid());
473
474 auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
475 // This is index mapped to ETM
476 const QModelIndex etmIndex = childrenProxy->mapToSource(childrenFilterIndex);
477 Q_ASSERT(etmIndex.isValid());
478 // We cannot possibly refer to top-level collection
479 Q_ASSERT(etmIndex.parent().isValid());
480
481 const auto col = etmIndex.parent().data(EntityTreeModel::CollectionRole).value<Collection>();
482 Q_ASSERT(col.isValid());
483
484 return col;
485}
486
487Akonadi::Collection MessageList::StorageModel::collectionForId(Akonadi::Collection::Id colId) const
488{
489 // Get ETM
490 auto childrenProxy = static_cast<QAbstractProxyModel *>(d->mChildrenFilterModel);
491 if (childrenProxy) {
492 QAbstractItemModel *etm = childrenProxy->sourceModel();
493 if (etm) {
494 // get index in EntityTreeModel
496 if (idx.isValid()) {
497 // get and return collection
499 }
500 }
501 }
502 return {};
503}
504
505void MessageList::StorageModel::resetModelStorage()
506{
507 beginResetModel();
508 endResetModel();
509}
510
511#include "moc_storagemodel.cpp"
static void registerAttribute()
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
QUrl url(UrlType type=UrlShort) const
Flags flags() const
bool hasPayload() const
Id id() const
T payload() const
Collection::Id storageCollectionId() const
QString remoteId() const
void setFlags(const Flags &flags)
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
The MessageItem class.
Definition messageitem.h:35
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)
QByteArray 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
void chop(qsizetype n)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString trimmed() const const
QString join(QChar separator) const const
void sort(Qt::CaseSensitivity cs)
Horizontal
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:54:19 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.