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

KDE's Doxygen guidelines are available online.