Merge branches 'fetch-binary-cte' and 'network-unavailable-and-offline-are-different'
[trojita.git] / src / Imap / Model / Model.cpp
blob4d7d5ea9f23ae1450f8a3ad2c7106b8f0892593c
1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
23 #include <QAbstractProxyModel>
24 #include <QAuthenticator>
25 #include <QCoreApplication>
26 #include <QDebug>
27 #include <QtAlgorithms>
28 #include "Model.h"
29 #include "Common/FindWithUnknown.h"
30 #include "Common/InvokeMethod.h"
31 #include "Imap/Encoders.h"
32 #include "Imap/Model/ItemRoles.h"
33 #include "Imap/Model/MailboxTree.h"
34 #include "Imap/Model/SpecialFlagNames.h"
35 #include "Imap/Model/TaskPresentationModel.h"
36 #include "Imap/Model/Utils.h"
37 #include "Imap/Tasks/AppendTask.h"
38 #include "Imap/Tasks/CreateMailboxTask.h"
39 #include "Imap/Tasks/GetAnyConnectionTask.h"
40 #include "Imap/Tasks/KeepMailboxOpenTask.h"
41 #include "Imap/Tasks/OpenConnectionTask.h"
42 #include "Imap/Tasks/UpdateFlagsTask.h"
43 #include "Streams/SocketFactory.h"
45 //#define DEBUG_PERIODICALLY_DUMP_TASKS
46 //#define DEBUG_TASK_ROUTING
48 namespace
51 using namespace Imap::Mailbox;
53 /** @short Return true iff the two mailboxes have the same name
55 It's an error to call this function on anything else but a mailbox.
57 bool MailboxNamesEqual(const TreeItem *const a, const TreeItem *const b)
59 const TreeItemMailbox *const mailboxA = dynamic_cast<const TreeItemMailbox *const>(a);
60 const TreeItemMailbox *const mailboxB = dynamic_cast<const TreeItemMailbox *const>(b);
61 Q_ASSERT(mailboxA);
62 Q_ASSERT(mailboxB);
64 return mailboxA->mailbox() == mailboxB->mailbox();
67 /** @short Mailbox name comparator to be used when sorting mailbox names
69 The special-case mailbox name, the "INBOX", is always sorted as the first one.
71 bool MailboxNameComparator(const TreeItem *const a, const TreeItem *const b)
73 const TreeItemMailbox *const mailboxA = dynamic_cast<const TreeItemMailbox *const>(a);
74 const TreeItemMailbox *const mailboxB = dynamic_cast<const TreeItemMailbox *const>(b);
76 if (mailboxA->mailbox() == QLatin1String("INBOX"))
77 return true;
78 if (mailboxB->mailbox() == QLatin1String("INBOX"))
79 return false;
80 return mailboxA->mailbox().compare(mailboxB->mailbox(), Qt::CaseInsensitive) < 1;
83 bool uidComparator(const TreeItem *const item, const uint uid)
85 const TreeItemMessage *const message = static_cast<const TreeItemMessage *const>(item);
86 uint messageUid = message->uid();
87 Q_ASSERT(messageUid);
88 return messageUid < uid;
91 bool messageHasUidZero(const TreeItem *const item)
93 const TreeItemMessage *const message = static_cast<const TreeItemMessage *const>(item);
94 return message->uid() == 0;
99 namespace Imap
101 namespace Mailbox
104 Model::Model(QObject *parent, std::shared_ptr<AbstractCache> cache, SocketFactoryPtr socketFactory, TaskFactoryPtr taskFactory)
105 : QAbstractItemModel(parent)
106 , m_cache(cache)
107 , m_socketFactory(std::move(socketFactory))
108 , m_taskFactory(std::move(taskFactory))
109 , m_maxParsers(4)
110 , m_mailboxes(nullptr)
111 , m_netPolicy(NETWORK_OFFLINE)
112 , m_taskModel(nullptr)
113 , m_hasImapPassword(PasswordAvailability::NOT_REQUESTED)
115 m_startTls = m_socketFactory->startTlsRequired();
117 m_mailboxes = new TreeItemMailbox(0);
119 onlineMessageFetch << QStringLiteral("ENVELOPE") << QStringLiteral("BODYSTRUCTURE") << QStringLiteral("RFC822.SIZE") <<
120 QStringLiteral("UID") << QStringLiteral("FLAGS");
122 EMIT_LATER_NOARG(this, networkPolicyOffline);
123 EMIT_LATER_NOARG(this, networkPolicyChanged);
125 #ifdef DEBUG_PERIODICALLY_DUMP_TASKS
126 QTimer *periodicTaskDumper = new QTimer(this);
127 periodicTaskDumper->setInterval(1000);
128 connect(periodicTaskDumper, &QTimer::timeout, this, &Model::slotTasksChanged);
129 periodicTaskDumper->start();
130 #endif
132 m_taskModel = new TaskPresentationModel(this);
134 m_periodicMailboxNumbersRefresh = new QTimer(this);
135 // polling every five minutes
136 m_periodicMailboxNumbersRefresh->setInterval(5 * 60 * 1000);
137 connect(m_periodicMailboxNumbersRefresh, &QTimer::timeout, this, &Model::invalidateAllMessageCounts);
140 Model::~Model()
142 delete m_mailboxes;
145 /** @short Process responses from all sockets */
146 void Model::responseReceived()
148 for (QMap<Parser *,ParserState>::iterator it = m_parsers.begin(); it != m_parsers.end(); ++it) {
149 responseReceived(it);
153 /** @short Process responses from the specified parser */
154 void Model::responseReceived(Parser *parser)
156 QMap<Parser *,ParserState>::iterator it = m_parsers.find(parser);
157 if (it == m_parsers.end()) {
158 // This is a queued signal, so it's perfectly possible that the sender is gone already
159 return;
161 responseReceived(it);
164 /** @short Process responses from the specified parser */
165 void Model::responseReceived(const QMap<Parser *,ParserState>::iterator it)
167 Q_ASSERT(it->parser);
169 int counter = 0;
170 while (it->parser && it->parser->hasResponse()) {
171 QSharedPointer<Imap::Responses::AbstractResponse> resp = it->parser->getResponse();
172 Q_ASSERT(resp);
173 // Always log BAD responses from a central place. They're bad enough to warant an extra treatment.
174 // FIXME: is it worth an UI popup?
175 if (Responses::State *stateResponse = dynamic_cast<Responses::State *>(resp.data())) {
176 if (stateResponse->kind == Responses::BAD) {
177 QString buf;
178 QTextStream s(&buf);
179 s << *stateResponse;
180 logTrace(it->parser->parserId(), Common::LOG_OTHER, QStringLiteral("Model"), QStringLiteral("BAD response: %1").arg(buf));
181 qDebug() << buf;
184 try {
185 /* At this point, we want to iterate over all active tasks and try them
186 for processing the server's responses (the plug() method). However, this
187 is rather complex -- this call to plug() could result in signals being
188 emitted, and certain slots connected to those signals might in turn want
189 to queue more Tasks. Therefore, it->activeTasks could be modified, some
190 items could be appended to it using the QList::append, which in turn could
191 cause a realloc to happen, happily invalidating our iterators, and that
192 kind of sucks.
194 So, we have to iterate over a copy of the original list and instead of
195 deleting Tasks, we store them into a temporary list. When we're done with
196 processing, we walk the original list once again and simply remove all
197 "deleted" items for real.
199 This took me 3+ hours to track it down to what the hell was happening here,
200 even though the underlying reason is simple -- QList::append() could invalidate
201 existing iterators.
204 bool handled = false;
205 QList<ImapTask *> taskSnapshot = it->activeTasks;
206 QList<ImapTask *> deletedTasks;
207 QList<ImapTask *>::const_iterator taskEnd = taskSnapshot.constEnd();
209 // Try various tasks, perhaps it's their response. Also check if they're already finished and remove them.
210 for (QList<ImapTask *>::const_iterator taskIt = taskSnapshot.constBegin(); taskIt != taskEnd; ++taskIt) {
211 if (! handled) {
213 #ifdef DEBUG_TASK_ROUTING
214 try {
215 logTrace(it->parser->parserId(), Common::LOG_TASKS, QString(),
216 QString::fromAscii("Routing to %1 %2").arg(QString::fromAscii((*taskIt)->metaObject()->className()),
217 (*taskIt)->debugIdentification()));
218 #endif
219 handled = resp->plug(*taskIt);
220 #ifdef DEBUG_TASK_ROUTING
221 if (handled) {
222 logTrace(it->parser->parserId(), Common::LOG_TASKS, (*taskIt)->debugIdentification(), QLatin1String("Handled"));
224 } catch (std::exception &e) {
225 logTrace(it->parser->parserId(), Common::LOG_TASKS, (*taskIt)->debugIdentification(), QLatin1String("Got exception when handling"));
226 throw;
228 #endif
231 if ((*taskIt)->isFinished()) {
232 deletedTasks << *taskIt;
236 removeDeletedTasks(deletedTasks, it->activeTasks);
238 runReadyTasks();
240 if (! handled) {
241 resp->plug(it->parser, this);
242 #ifdef DEBUG_TASK_ROUTING
243 if (it->parser) {
244 logTrace(it->parser->parserId(), Common::LOG_TASKS, QLatin1String("Model"), QLatin1String("Handled"));
245 } else {
246 logTrace(0, Common::LOG_TASKS, QLatin1String("Model"), QLatin1String("Handled"));
248 #endif
250 } catch (Imap::ImapException &e) {
251 uint parserId = it->parser->parserId();
252 killParser(it->parser, PARSER_KILL_HARD);
253 broadcastParseError(parserId, QString::fromStdString(e.exceptionClass()), QString::fromUtf8(e.what()), e.line(), e.offset());
254 break;
257 // Return to the event loop every 100 messages to handle GUI events
258 ++counter;
259 if (counter == 100) {
260 QTimer::singleShot(0, this, SLOT(responseReceived()));
261 break;
265 if (!it->parser) {
266 // He's dead, Jim
267 m_taskModel->beginResetModel();
268 killParser(it.key(), PARSER_JUST_DELETE_LATER);
269 m_parsers.erase(it);
270 m_taskModel->endResetModel();
274 void Model::handleState(Imap::Parser *ptr, const Imap::Responses::State *const resp)
276 // OK/NO/BAD/PREAUTH/BYE
277 using namespace Imap::Responses;
279 const QByteArray &tag = resp->tag;
281 if (!tag.isEmpty()) {
282 if (tag == accessParser(ptr).logoutCmd) {
283 // The LOGOUT is special, as it isn't associated with any task
284 killParser(ptr, PARSER_KILL_EXPECTED);
285 } else {
286 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
287 return;
288 // Unhandled command -- this is *extremely* weird
289 throw CantHappen("The following command should have been handled elsewhere", *resp);
291 } else {
292 // untagged response
293 // FIXME: we should probably just eat them and don't bother, as untagged OK/NO could be rather common...
294 switch (resp->kind) {
295 case BYE:
296 if (accessParser(ptr).logoutCmd.isEmpty()) {
297 // The connection got closed but we haven't really requested that -- we better treat that as error, including
298 // going offline...
299 // ... but before that, expect that the connection will get closed soon
300 changeConnectionState(ptr, CONN_STATE_LOGOUT);
301 EMIT_LATER(this, imapError, Q_ARG(QString, resp->message));
302 setNetworkPolicy(NETWORK_OFFLINE);
304 if (accessParser(ptr).parser) {
305 // previous block could enter the event loop and hence kill our parser; we shouldn't try to kill it twice
306 killParser(ptr, PARSER_KILL_EXPECTED);
308 break;
309 case OK:
310 if (resp->respCode == NONE) {
311 // This one probably should not be logged at all; dovecot sends these reponses to keep NATted connections alive
312 break;
313 } else {
314 logTrace(ptr->parserId(), Common::LOG_OTHER, QString(), QStringLiteral("Warning: unhandled untagged OK with a response code"));
315 break;
317 case NO:
318 logTrace(ptr->parserId(), Common::LOG_OTHER, QString(), QStringLiteral("Warning: unhandled untagged NO..."));
319 break;
320 default:
321 throw UnexpectedResponseReceived("Unhandled untagged response, sorry", *resp);
326 void Model::finalizeList(Parser *parser, TreeItemMailbox *mailboxPtr)
328 TreeItemChildrenList mailboxes;
330 QList<Responses::List> &listResponses = accessParser(parser).listResponses;
331 const QString prefix = mailboxPtr->mailbox() + mailboxPtr->separator();
332 for (QList<Responses::List>::iterator it = listResponses.begin(); it != listResponses.end(); /* nothing */) {
333 if (it->mailbox == mailboxPtr->mailbox() || it->mailbox == prefix) {
334 // rubbish, ignore
335 it = listResponses.erase(it);
336 } else if (it->mailbox.startsWith(prefix)) {
337 if (!mailboxPtr->separator().isEmpty() && it->mailbox.midRef(prefix.length()).contains(mailboxPtr->separator())) {
338 // This is about a mailbox which is nested deeper beneath the current thing (e.g., we're listing A.B.%,
339 // and the current response is A.B.C.1), so let's assume that it's some else's LIST response.
340 // The separator/delimiter checking is (hopefully) correct -- see https://tools.ietf.org/html/rfc3501#page-70 .
341 ++it;
342 } else {
343 mailboxes << new TreeItemMailbox(mailboxPtr, *it);
344 it = listResponses.erase(it);
346 } else {
347 // it clearly is someone else's LIST response
348 ++it;
351 qSort(mailboxes.begin(), mailboxes.end(), MailboxNameComparator);
353 // Remove duplicates; would be great if this could be done in a STLish way,
354 // but unfortunately std::unique won't help here (the "duped" part of the
355 // sequence contains undefined items)
356 if (mailboxes.size() > 1) {
357 auto it = mailboxes.begin();
358 // We've got to ignore the first one, that's the message list
359 ++it;
360 while (it != mailboxes.end()) {
361 if (MailboxNamesEqual(it[-1], *it)) {
362 delete *it;
363 it = mailboxes.erase(it);
364 } else {
365 ++it;
370 QList<MailboxMetadata> metadataToCache;
371 TreeItemChildrenList mailboxesWithoutChildren;
372 for (auto it = mailboxes.constBegin(); it != mailboxes.constEnd(); ++it) {
373 TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(*it);
374 Q_ASSERT(mailbox);
375 metadataToCache.append(mailbox->mailboxMetadata());
376 if (mailbox->hasNoChildMailboxesAlreadyKnown()) {
377 mailboxesWithoutChildren << mailbox;
380 cache()->setChildMailboxes(mailboxPtr->mailbox(), metadataToCache);
381 for (auto it = mailboxesWithoutChildren.constBegin(); it != mailboxesWithoutChildren.constEnd(); ++it)
382 cache()->setChildMailboxes(static_cast<TreeItemMailbox *>(*it)->mailbox(), QList<MailboxMetadata>());
383 replaceChildMailboxes(mailboxPtr, mailboxes);
386 void Model::finalizeIncrementalList(Parser *parser, const QString &parentMailboxName)
388 TreeItemMailbox *parentMbox = findParentMailboxByName(parentMailboxName);
389 if (! parentMbox) {
390 qDebug() << "Weird, no idea where to put the newly created mailbox" << parentMailboxName;
391 return;
394 QList<TreeItem *> mailboxes;
396 QList<Responses::List> &listResponses = accessParser(parser).listResponses;
397 for (QList<Responses::List>::iterator it = listResponses.begin();
398 it != listResponses.end(); /* nothing */) {
399 if (it->mailbox == parentMailboxName) {
400 mailboxes << new TreeItemMailbox(parentMbox, *it);
401 it = listResponses.erase(it);
402 } else {
403 // it clearly is someone else's LIST response
404 ++it;
407 qSort(mailboxes.begin(), mailboxes.end(), MailboxNameComparator);
409 if (mailboxes.size() == 0) {
410 qDebug() << "Weird, no matching LIST response for our prompt after CREATE";
411 qDeleteAll(mailboxes);
412 return;
413 } else if (mailboxes.size() > 1) {
414 qDebug() << "Weird, too many LIST responses for our prompt after CREATE";
415 qDeleteAll(mailboxes);
416 return;
419 auto it = parentMbox->m_children.begin();
420 Q_ASSERT(it != parentMbox->m_children.end());
421 ++it;
422 while (it != parentMbox->m_children.end() && MailboxNameComparator(*it, mailboxes[0]))
423 ++it;
424 QModelIndex parentIdx = parentMbox == m_mailboxes ? QModelIndex() : parentMbox->toIndex(this);
425 if (it == parentMbox->m_children.end())
426 beginInsertRows(parentIdx, parentMbox->m_children.size(), parentMbox->m_children.size());
427 else
428 beginInsertRows(parentIdx, (*it)->row(), (*it)->row());
429 parentMbox->m_children.insert(it, mailboxes[0]);
430 endInsertRows();
433 void Model::replaceChildMailboxes(TreeItemMailbox *mailboxPtr, const TreeItemChildrenList &mailboxes)
435 /* Previously, we would call layoutAboutToBeChanged() and layoutChanged() here, but it
436 resulted in invalid memory access in the attached QSortFilterProxyModels like this one:
438 ==23294== Invalid read of size 4
439 ==23294== at 0x5EA34B1: QSortFilterProxyModelPrivate::index_to_iterator(QModelIndex const&) const (qsortfilterproxymodel.cpp:191)
440 ==23294== by 0x5E9F8A3: QSortFilterProxyModel::parent(QModelIndex const&) const (qsortfilterproxymodel.cpp:1654)
441 ==23294== by 0x5C5D45D: QModelIndex::parent() const (qabstractitemmodel.h:389)
442 ==23294== by 0x5E47C48: QTreeView::drawRow(QPainter*, QStyleOptionViewItem const&, QModelIndex const&) const (qtreeview.cpp:1479)
443 ==23294== by 0x5E479D9: QTreeView::drawTree(QPainter*, QRegion const&) const (qtreeview.cpp:1441)
444 ==23294== by 0x5E4703A: QTreeView::paintEvent(QPaintEvent*) (qtreeview.cpp:1274)
445 ==23294== by 0x5810C30: QWidget::event(QEvent*) (qwidget.cpp:8346)
446 ==23294== by 0x5C91D03: QFrame::event(QEvent*) (qframe.cpp:557)
447 ==23294== by 0x5D4259C: QAbstractScrollArea::viewportEvent(QEvent*) (qabstractscrollarea.cpp:1043)
448 ==23294== by 0x5DFFD6E: QAbstractItemView::viewportEvent(QEvent*) (qabstractitemview.cpp:1619)
449 ==23294== by 0x5E46EE0: QTreeView::viewportEvent(QEvent*) (qtreeview.cpp:1256)
450 ==23294== by 0x5D43110: QAbstractScrollAreaPrivate::viewportEvent(QEvent*) (qabstractscrollarea_p.h:100)
451 ==23294== Address 0x908dbec is 20 bytes inside a block of size 24 free'd
452 ==23294== at 0x4024D74: operator delete(void*) (vg_replace_malloc.c:346)
453 ==23294== by 0x5EA5236: void qDeleteAll<QHash<QModelIndex, QSortFilterProxyModelPrivate::Mapping*>::const_iterator>(QHash<QModelIndex, QSortFilterProxyModelPrivate::Mapping*>::const_iterator, QHash<QModelIndex, QSortFilterProxyModelPrivate::Mapping*>::const_iterator) (qalgorithms.h:322)
454 ==23294== by 0x5EA3C06: void qDeleteAll<QHash<QModelIndex, QSortFilterProxyModelPrivate::Mapping*> >(QHash<QModelIndex, QSortFilterProxyModelPrivate::Mapping*> const&) (qalgorithms.h:330)
455 ==23294== by 0x5E9E64B: QSortFilterProxyModelPrivate::_q_sourceLayoutChanged() (qsortfilterproxymodel.cpp:1249)
456 ==23294== by 0x5EA29EC: QSortFilterProxyModel::qt_metacall(QMetaObject::Call, int, void**) (moc_qsortfilterproxymodel.cpp:133)
457 ==23294== by 0x80EB205: Imap::Mailbox::PrettyMailboxModel::qt_metacall(QMetaObject::Call, int, void**) (moc_PrettyMailboxModel.cpp:64)
458 ==23294== by 0x65D3EAD: QMetaObject::metacall(QObject*, QMetaObject::Call, int, void**) (qmetaobject.cpp:237)
459 ==23294== by 0x65E8D7C: QMetaObject::activate(QObject*, QMetaObject const*, int, void**) (qobject.cpp:3272)
460 ==23294== by 0x664A7E8: QAbstractItemModel::layoutChanged() (moc_qabstractitemmodel.cpp:161)
461 ==23294== by 0x664A354: QAbstractItemModel::qt_metacall(QMetaObject::Call, int, void**) (moc_qabstractitemmodel.cpp:118)
462 ==23294== by 0x5E9A3A9: QAbstractProxyModel::qt_metacall(QMetaObject::Call, int, void**) (moc_qabstractproxymodel.cpp:67)
463 ==23294== by 0x80EAF3D: Imap::Mailbox::MailboxModel::qt_metacall(QMetaObject::Call, int, void**) (moc_MailboxModel.cpp:81)
465 I have no idea why something like that happens -- layoutChanged() should be a hint that the indexes are gone now, which means
466 that the code should *not* use tham after that point. That's just weird.
469 QModelIndex parent = mailboxPtr == m_mailboxes ? QModelIndex() : mailboxPtr->toIndex(this);
471 if (mailboxPtr->m_children.size() != 1) {
472 // There's something besides the TreeItemMsgList and we're going to
473 // overwrite them, so we have to delete them right now
474 int count = mailboxPtr->rowCount(this);
475 beginRemoveRows(parent, 1, count - 1);
476 auto oldItems = mailboxPtr->setChildren(TreeItemChildrenList());
477 endRemoveRows();
479 qDeleteAll(oldItems);
482 if (! mailboxes.isEmpty()) {
483 beginInsertRows(parent, 1, mailboxes.size());
484 auto dummy = mailboxPtr->setChildren(mailboxes);
485 endInsertRows();
486 Q_ASSERT(dummy.isEmpty());
487 } else {
488 auto dummy = mailboxPtr->setChildren(mailboxes);
489 Q_ASSERT(dummy.isEmpty());
491 emit dataChanged(parent, parent);
494 void Model::emitMessageCountChanged(TreeItemMailbox *const mailbox)
496 TreeItemMsgList *list = static_cast<TreeItemMsgList *>(mailbox->m_children[0]);
497 QModelIndex msgListIndex = list->toIndex(this);
498 emit dataChanged(msgListIndex, msgListIndex);
499 QModelIndex mailboxIndex = mailbox->toIndex(this);
500 emit dataChanged(mailboxIndex, mailboxIndex);
501 emit messageCountPossiblyChanged(mailboxIndex);
504 void Model::handleCapability(Imap::Parser *ptr, const Imap::Responses::Capability *const resp)
506 updateCapabilities(ptr, resp->capabilities);
509 void Model::handleNumberResponse(Imap::Parser *ptr, const Imap::Responses::NumberResponse *const resp)
511 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
512 return;
513 throw UnexpectedResponseReceived("[Tasks API Port] Unhandled NumberResponse", *resp);
516 void Model::handleList(Imap::Parser *ptr, const Imap::Responses::List *const resp)
518 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
519 return;
520 accessParser(ptr).listResponses << *resp;
523 void Model::handleFlags(Imap::Parser *ptr, const Imap::Responses::Flags *const resp)
525 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
526 return;
527 throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Flags", *resp);
530 void Model::handleSearch(Imap::Parser *ptr, const Imap::Responses::Search *const resp)
532 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
533 return;
534 throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Search", *resp);
537 void Model::handleESearch(Imap::Parser *ptr, const Imap::Responses::ESearch *const resp)
539 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
540 return;
541 throw UnexpectedResponseReceived("Unhandled ESEARCH", *resp);
544 void Model::handleStatus(Imap::Parser *ptr, const Imap::Responses::Status *const resp)
546 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
547 return;
548 Q_UNUSED(ptr);
549 TreeItemMailbox *mailbox = findMailboxByName(resp->mailbox);
550 if (! mailbox) {
551 qDebug() << "Couldn't find out which mailbox is" << resp->mailbox << "when parsing a STATUS reply";
552 return;
554 TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[0]);
555 Q_ASSERT(list);
556 bool updateCache = false;
557 Imap::Responses::Status::stateDataType::const_iterator it = resp->states.constEnd();
558 if ((it = resp->states.constFind(Imap::Responses::Status::MESSAGES)) != resp->states.constEnd()) {
559 updateCache |= list->m_totalMessageCount != static_cast<const int>(it.value());
560 list->m_totalMessageCount = it.value();
562 if ((it = resp->states.constFind(Imap::Responses::Status::UNSEEN)) != resp->states.constEnd()) {
563 updateCache |= list->m_unreadMessageCount != static_cast<const int>(it.value());
564 list->m_unreadMessageCount = it.value();
566 if ((it = resp->states.constFind(Imap::Responses::Status::RECENT)) != resp->states.constEnd()) {
567 updateCache |= list->m_recentMessageCount != static_cast<const int>(it.value());
568 list->m_recentMessageCount = it.value();
570 list->m_numberFetchingStatus = TreeItem::DONE;
571 emitMessageCountChanged(mailbox);
573 if (updateCache) {
574 // We have to be very careful to only touch the bits which are *not* used by the mailbox syncing code.
575 // This is absolutely crucial -- STATUS is just a meaningless indicator, and stuff like the UID mapping
576 // is definitely *not* updated at the same time. That's also why we completely ignore any data whatsoever
577 // from the TreeItemMailbox' syncState, if any, and just work with the cache directly.
578 auto state = cache()->mailboxSyncState(mailbox->mailbox());
579 // We are *not* updating the total message count as that conflicts with the mailbox syncing
580 if (list->m_unreadMessageCount != -1) {
581 state.setUnSeenCount(list->m_unreadMessageCount);
583 if (list->m_recentMessageCount != -1) {
584 state.setRecent(list->m_recentMessageCount);
586 cache()->setMailboxSyncState(mailbox->mailbox(), state);
590 void Model::handleFetch(Imap::Parser *ptr, const Imap::Responses::Fetch *const resp)
592 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
593 return;
594 throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Fetch", *resp);
597 void Model::handleNamespace(Imap::Parser *ptr, const Imap::Responses::Namespace *const resp)
599 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
600 return;
601 return; // because it's broken and won't fly
602 Q_UNUSED(ptr);
603 Q_UNUSED(resp);
606 void Model::handleSort(Imap::Parser *ptr, const Imap::Responses::Sort *const resp)
608 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
609 return;
610 throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Sort", *resp);
613 void Model::handleThread(Imap::Parser *ptr, const Imap::Responses::Thread *const resp)
615 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
616 return;
617 throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Thread", *resp);
620 void Model::handleId(Parser *ptr, const Responses::Id *const resp)
622 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
623 return;
624 throw UnexpectedResponseReceived("Unhandled ID response", *resp);
627 void Model::handleEnabled(Parser *ptr, const Responses::Enabled *const resp)
629 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
630 return;
631 throw UnexpectedResponseReceived("Unhandled ENABLED response", *resp);
634 void Model::handleVanished(Parser *ptr, const Responses::Vanished *const resp)
636 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
637 return;
638 throw UnexpectedResponseReceived("Unhandled VANISHED response", *resp);
641 void Model::handleGenUrlAuth(Parser *ptr, const Responses::GenUrlAuth *const resp)
643 if (accessParser(ptr).connState == CONN_STATE_LOGOUT)
644 return;
645 throw UnexpectedResponseReceived("Unhandled GENURLAUTH response", *resp);
648 void Model::handleSocketEncryptedResponse(Parser *ptr, const Responses::SocketEncryptedResponse *const resp)
650 Q_UNUSED(resp);
651 logTrace(ptr->parserId(), Common::LOG_IO_READ, QStringLiteral("Model"), QStringLiteral("Information about the SSL state not handled by the upper layers -- disconnect?"));
652 killParser(ptr, PARSER_KILL_EXPECTED);
655 TreeItem *Model::translatePtr(const QModelIndex &index) const
657 return index.internalPointer() ? static_cast<TreeItem *>(index.internalPointer()) : m_mailboxes;
660 QVariant Model::data(const QModelIndex &index, int role) const
662 if (role == RoleIsNetworkOffline)
663 return !isNetworkAvailable();
665 return translatePtr(index)->data(const_cast<Model *>(this), role);
668 QModelIndex Model::index(int row, int column, const QModelIndex &parent) const
670 if (parent.isValid()) {
671 Q_ASSERT(parent.model() == this);
674 TreeItem *parentItem = translatePtr(parent);
676 // Deal with the possibility of an "irregular shape" of our model here.
677 // The issue is that some items have child items not only in column #0
678 // and in specified number of rows, but also in row #0 and various columns.
679 if (column != 0) {
680 TreeItem *item = parentItem->specialColumnPtr(row, column);
681 if (item)
682 return QAbstractItemModel::createIndex(row, column, item);
683 else
684 return QModelIndex();
687 TreeItem *child = parentItem->child(row, const_cast<Model *>(this));
689 return child ? QAbstractItemModel::createIndex(row, column, child) : QModelIndex();
692 QModelIndex Model::parent(const QModelIndex &index) const
694 if (!index.isValid())
695 return QModelIndex();
697 Q_ASSERT(index.model() == this);
699 TreeItem *childItem = static_cast<TreeItem *>(index.internalPointer());
700 TreeItem *parentItem = childItem->parent();
702 if (! parentItem || parentItem == m_mailboxes)
703 return QModelIndex();
705 return QAbstractItemModel::createIndex(parentItem->row(), 0, parentItem);
708 int Model::rowCount(const QModelIndex &index) const
710 TreeItem *node = static_cast<TreeItem *>(index.internalPointer());
711 if (!node) {
712 node = m_mailboxes;
713 } else {
714 Q_ASSERT(index.model() == this);
716 Q_ASSERT(node);
717 return node->rowCount(const_cast<Model *>(this));
720 int Model::columnCount(const QModelIndex &index) const
722 TreeItem *node = static_cast<TreeItem *>(index.internalPointer());
723 if (!node) {
724 node = m_mailboxes;
725 } else {
726 Q_ASSERT(index.model() == this);
728 Q_ASSERT(node);
729 return node->columnCount();
732 bool Model::hasChildren(const QModelIndex &parent) const
734 if (parent.isValid()) {
735 Q_ASSERT(parent.model() == this);
738 TreeItem *node = translatePtr(parent);
740 if (node)
741 return node->hasChildren(const_cast<Model *>(this));
742 else
743 return false;
746 void Model::askForChildrenOfMailbox(const QModelIndex &index, const CacheLoadingMode cacheMode)
748 TreeItemMailbox *mailbox = 0;
749 if (index.isValid()) {
750 Q_ASSERT(index.model() == this);
751 mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(index.internalPointer()));
752 } else {
753 mailbox = m_mailboxes;
755 Q_ASSERT(mailbox);
756 askForChildrenOfMailbox(mailbox, cacheMode == LOAD_FORCE_RELOAD);
759 void Model::askForMessagesInMailbox(const QModelIndex &index)
761 if (!index.isValid())
762 return;
763 Q_ASSERT(index.model() == this);
764 auto msgList = dynamic_cast<TreeItemMsgList *>(static_cast<TreeItem *>(index.internalPointer()));
765 Q_ASSERT(msgList);
766 askForMessagesInMailbox(msgList);
769 void Model::askForChildrenOfMailbox(TreeItemMailbox *item, bool forceReload)
771 if (!forceReload && cache()->childMailboxesFresh(item->mailbox())) {
772 // The permanent cache contains relevant data
773 QList<MailboxMetadata> metadata = cache()->childMailboxes(item->mailbox());
774 TreeItemChildrenList mailboxes;
775 for (QList<MailboxMetadata>::const_iterator it = metadata.constBegin(); it != metadata.constEnd(); ++it) {
776 TreeItemMailbox *mailbox = TreeItemMailbox::fromMetadata(item, *it);
777 TreeItemMsgList *list = dynamic_cast<TreeItemMsgList*>(mailbox->m_children[0]);
778 Q_ASSERT(list);
779 Imap::Mailbox::SyncState syncState = cache()->mailboxSyncState(mailbox->mailbox());
780 if (syncState.isUsableForNumbers()) {
781 list->m_unreadMessageCount = syncState.unSeenCount();
782 list->m_totalMessageCount = syncState.exists();
783 list->m_recentMessageCount = syncState.recent();
784 list->m_numberFetchingStatus = TreeItem::LOADING;
785 } else {
786 list->m_numberFetchingStatus = TreeItem::UNAVAILABLE;
788 mailboxes << mailbox;
790 TreeItemMailbox *mailboxPtr = dynamic_cast<TreeItemMailbox *>(item);
791 Q_ASSERT(mailboxPtr);
792 replaceChildMailboxes(mailboxPtr, mailboxes);
793 } else if (networkPolicy() == NETWORK_OFFLINE) {
794 // No cached data, no network -> fail
795 item->setFetchStatus(TreeItem::UNAVAILABLE);
796 QModelIndex idx = item->toIndex(this);
797 emit dataChanged(idx, idx);
798 return;
801 // We shall ask the network
802 m_taskFactory->createListChildMailboxesTask(this, item->toIndex(this));
804 QModelIndex idx = item->toIndex(this);
805 emit dataChanged(idx, idx);
808 void Model::reloadMailboxList()
810 m_mailboxes->rescanForChildMailboxes(this);
813 void Model::askForMessagesInMailbox(TreeItemMsgList *item)
815 Q_ASSERT(item->parent());
816 TreeItemMailbox *mailboxPtr = dynamic_cast<TreeItemMailbox *>(item->parent());
817 Q_ASSERT(mailboxPtr);
819 QString mailbox = mailboxPtr->mailbox();
821 Q_ASSERT(item->m_children.size() == 0);
823 auto uidMapping = cache()->uidMapping(mailbox);
824 auto oldSyncState = cache()->mailboxSyncState(mailbox);
825 if (networkPolicy() == NETWORK_OFFLINE && oldSyncState.isUsableForSyncing()
826 && static_cast<uint>(uidMapping.size()) != oldSyncState.exists()) {
827 // Problem with the cached data
828 qDebug() << "UID cache stale for mailbox" << mailbox
829 << "(" << uidMapping.size() << "in UID cache vs." << oldSyncState.exists() << "in the sync state and"
830 << item->m_totalMessageCount << "as totalMessageCount (possibly updated by STATUS))";
831 item->setFetchStatus(TreeItem::UNAVAILABLE);
832 } else if (networkPolicy() == NETWORK_OFFLINE && !oldSyncState.isUsableForSyncing()) {
833 // Nothing in the cache
834 item->setFetchStatus(TreeItem::UNAVAILABLE);
835 } else if (oldSyncState.isUsableForSyncing()) {
836 // We can pre-populate the tree with data from cache
837 Q_ASSERT(item->m_children.isEmpty());
838 Q_ASSERT(item->accessFetchStatus() == TreeItem::LOADING);
839 QModelIndex listIndex = item->toIndex(this);
840 if (uidMapping.size()) {
841 beginInsertRows(listIndex, 0, uidMapping.size() - 1);
842 for (uint seq = 0; seq < static_cast<uint>(uidMapping.size()); ++seq) {
843 TreeItemMessage *message = new TreeItemMessage(item);
844 message->m_offset = seq;
845 message->m_uid = uidMapping[seq];
846 item->m_children << message;
847 QStringList flags = cache()->msgFlags(mailbox, message->m_uid);
848 flags.removeOne(QStringLiteral("\\Recent"));
849 message->m_flags = normalizeFlags(flags);
851 endInsertRows();
853 mailboxPtr->syncState = oldSyncState;
854 item->setFetchStatus(TreeItem::DONE); // required for FETCH processing later on
855 // The list of messages was satisfied from cache. Do the same for the message counts, if applicable
856 item->recalcVariousMessageCounts(this);
859 if (networkPolicy() != NETWORK_OFFLINE) {
860 findTaskResponsibleFor(mailboxPtr);
861 // and that's all -- the task will detect following replies and sync automatically
865 void Model::askForNumberOfMessages(TreeItemMsgList *item)
867 Q_ASSERT(item->parent());
868 TreeItemMailbox *mailboxPtr = dynamic_cast<TreeItemMailbox *>(item->parent());
869 Q_ASSERT(mailboxPtr);
871 if (networkPolicy() == NETWORK_OFFLINE) {
872 Imap::Mailbox::SyncState syncState = cache()->mailboxSyncState(mailboxPtr->mailbox());
873 if (syncState.isUsableForNumbers()) {
874 item->m_unreadMessageCount = syncState.unSeenCount();
875 item->m_totalMessageCount = syncState.exists();
876 item->m_recentMessageCount = syncState.recent();
877 item->m_numberFetchingStatus = TreeItem::DONE;
878 emitMessageCountChanged(mailboxPtr);
879 } else {
880 item->m_numberFetchingStatus = TreeItem::UNAVAILABLE;
882 } else {
883 m_taskFactory->createNumberOfMessagesTask(this, mailboxPtr->toIndex(this));
887 void Model::askForMsgMetadata(TreeItemMessage *item, const PreloadingMode preloadMode)
889 Q_ASSERT(item->uid());
890 Q_ASSERT(!item->fetched());
891 TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(item->parent());
892 Q_ASSERT(list);
893 TreeItemMailbox *mailboxPtr = dynamic_cast<TreeItemMailbox *>(list->parent());
894 Q_ASSERT(mailboxPtr);
896 if (item->uid()) {
897 AbstractCache::MessageDataBundle data = cache()->messageMetadata(mailboxPtr->mailbox(), item->uid());
898 if (data.uid == item->uid()) {
899 item->data()->setEnvelope(data.envelope);
900 item->data()->setSize(data.size);
901 item->data()->setHdrReferences(data.hdrReferences);
902 item->data()->setHdrListPost(data.hdrListPost);
903 item->data()->setHdrListPostNo(data.hdrListPostNo);
904 QDataStream stream(&data.serializedBodyStructure, QIODevice::ReadOnly);
905 stream.setVersion(QDataStream::Qt_4_6);
906 QVariantList unserialized;
907 stream >> unserialized;
908 QSharedPointer<Message::AbstractMessage> abstractMessage;
909 try {
910 abstractMessage = Message::AbstractMessage::fromList(unserialized, QByteArray(), 0);
911 } catch (Imap::ParserException &e) {
912 qDebug() << "Error when parsing cached BODYSTRUCTURE" << e.what();
914 if (! abstractMessage) {
915 item->setFetchStatus(TreeItem::UNAVAILABLE);
916 } else {
917 auto newChildren = abstractMessage->createTreeItems(item);
918 if (item->m_children.isEmpty()) {
919 TreeItemChildrenList oldChildren = item->setChildren(newChildren);
920 Q_ASSERT(oldChildren.size() == 0);
921 } else {
922 // The following assert guards against that crazy signal emitting we had when various askFor*()
923 // functions were not delayed. If it gets hit, it means that someone tried to call this function
924 // on an item which was already loaded.
925 Q_ASSERT(item->m_children.isEmpty());
926 item->setChildren(newChildren);
928 item->setFetchStatus(TreeItem::DONE);
933 switch (networkPolicy()) {
934 case NETWORK_OFFLINE:
935 if (item->accessFetchStatus() != TreeItem::DONE)
936 item->setFetchStatus(TreeItem::UNAVAILABLE);
937 break;
938 case NETWORK_EXPENSIVE:
939 if (item->accessFetchStatus() != TreeItem::DONE) {
940 item->setFetchStatus(TreeItem::LOADING);
941 findTaskResponsibleFor(mailboxPtr)->requestEnvelopeDownload(item->uid());
943 break;
944 case NETWORK_ONLINE:
946 if (item->accessFetchStatus() != TreeItem::DONE) {
947 item->setFetchStatus(TreeItem::LOADING);
948 findTaskResponsibleFor(mailboxPtr)->requestEnvelopeDownload(item->uid());
951 // preload
952 if (preloadMode != PRELOAD_PER_POLICY)
953 break;
954 bool ok;
955 int preload = property("trojita-imap-preload-msg-metadata").toInt(&ok);
956 if (! ok)
957 preload = 50;
958 int order = item->row();
959 for (int i = qMax(0, order - preload); i < qMin(list->m_children.size(), order + preload); ++i) {
960 TreeItemMessage *message = dynamic_cast<TreeItemMessage *>(list->m_children[i]);
961 Q_ASSERT(message);
962 if (item != message && !message->fetched() && !message->loading() && message->uid()) {
963 message->setFetchStatus(TreeItem::LOADING);
964 // cannot ask the KeepTask directly, that'd completely ignore the cache
965 // but we absolutely have to block the preload :)
966 askForMsgMetadata(message, PRELOAD_DISABLED);
970 break;
972 EMIT_LATER(this, dataChanged, Q_ARG(QModelIndex, item->toIndex(this)), Q_ARG(QModelIndex, item->toIndex(this)));
975 void Model::askForMsgPart(TreeItemPart *item, bool onlyFromCache)
977 Q_ASSERT(item->message()); // TreeItemMessage
978 Q_ASSERT(item->message()->parent()); // TreeItemMsgList
979 Q_ASSERT(item->message()->parent()->parent()); // TreeItemMailbox
980 TreeItemMailbox *mailboxPtr = dynamic_cast<TreeItemMailbox *>(item->message()->parent()->parent());
981 Q_ASSERT(mailboxPtr);
983 // We are asking for a message part, which means that the structure of a message is already known.
984 // If the UID was zero at this point, it would mean that we are completely doomed.
985 uint uid = static_cast<TreeItemMessage *>(item->message())->uid();
986 Q_ASSERT(uid);
988 // Check whether this is a request for fetching the special item representing the raw contents prior to any CTE undoing
989 TreeItemPart *itemForFetchOperation = item;
990 TreeItemModifiedPart *modifiedPart = dynamic_cast<TreeItemModifiedPart*>(item);
991 bool isSpecialRawPart = modifiedPart && modifiedPart->kind() == TreeItem::OFFSET_RAW_CONTENTS;
992 if (isSpecialRawPart) {
993 itemForFetchOperation = dynamic_cast<TreeItemPart*>(item->parent());
994 Q_ASSERT(itemForFetchOperation);
997 const QByteArray &data = cache()->messagePart(mailboxPtr->mailbox(), uid,
998 isSpecialRawPart ?
999 itemForFetchOperation->partId() + ".X-RAW"
1000 : item->partId());
1001 if (! data.isNull()) {
1002 item->m_data = data;
1003 item->setFetchStatus(TreeItem::DONE);
1004 return;
1007 if (!isSpecialRawPart) {
1008 const QByteArray &data = cache()->messagePart(mailboxPtr->mailbox(), uid,
1009 itemForFetchOperation->partId() + ".X-RAW");
1011 if (!data.isNull()) {
1012 Imap::decodeContentTransferEncoding(data, item->transferEncoding(), item->dataPtr());
1013 item->setFetchStatus(TreeItem::DONE);
1014 return;
1017 if (item->m_partRaw && item->m_partRaw->loading()) {
1018 // There's already a request for the raw data. Let's use it and don't queue an extra fetch here.
1019 item->setFetchStatus(TreeItem::LOADING);
1020 return;
1024 if (networkPolicy() == NETWORK_OFFLINE) {
1025 if (item->accessFetchStatus() != TreeItem::DONE)
1026 item->setFetchStatus(TreeItem::UNAVAILABLE);
1027 } else if (! onlyFromCache) {
1028 KeepMailboxOpenTask *keepTask = findTaskResponsibleFor(mailboxPtr);
1029 TreeItemPart::PartFetchingMode fetchingMode = TreeItemPart::FETCH_PART_IMAP;
1030 if (!isSpecialRawPart && keepTask->parser && accessParser(keepTask->parser).capabilitiesFresh &&
1031 accessParser(keepTask->parser).capabilities.contains(QStringLiteral("BINARY"))) {
1032 if (!item->hasChildren(0) && !item->m_binaryCTEFailed) {
1033 // The BINARY only actually makes sense on leaf MIME nodes
1034 fetchingMode = TreeItemPart::FETCH_PART_BINARY;
1037 keepTask->requestPartDownload(item->message()->m_uid, itemForFetchOperation->partIdForFetch(fetchingMode), item->octets());
1041 void Model::resyncMailbox(const QModelIndex &mbox)
1043 findTaskResponsibleFor(mbox)->resynchronizeMailbox();
1046 void Model::setNetworkPolicy(const NetworkPolicy policy)
1048 bool networkReconnected = m_netPolicy == NETWORK_OFFLINE && policy != NETWORK_OFFLINE;
1049 switch (policy) {
1050 case NETWORK_OFFLINE:
1051 for (QMap<Parser *,ParserState>::iterator it = m_parsers.begin(); it != m_parsers.end(); ++it) {
1052 if (!it->parser || it->connState == CONN_STATE_LOGOUT) {
1053 // there's no point in sending LOGOUT over these
1054 continue;
1056 Q_ASSERT(it->parser);
1057 if (it->maintainingTask) {
1058 // First of all, give the maintaining task a chance to finish its housekeeping
1059 it->maintainingTask->stopForLogout();
1061 // Kill all tasks that are also using this connection
1062 Q_FOREACH(ImapTask *task, it->activeTasks) {
1063 task->die(tr("Going offline"));
1065 it->logoutCmd = it->parser->logout();
1066 changeConnectionState(it->parser, CONN_STATE_LOGOUT);
1068 m_netPolicy = NETWORK_OFFLINE;
1069 m_periodicMailboxNumbersRefresh->stop();
1070 emit networkPolicyChanged();
1071 emit networkPolicyOffline();
1073 // FIXME: kill the connection
1074 break;
1075 case NETWORK_EXPENSIVE:
1076 m_netPolicy = NETWORK_EXPENSIVE;
1077 m_periodicMailboxNumbersRefresh->stop();
1078 emit networkPolicyChanged();
1079 emit networkPolicyExpensive();
1080 break;
1081 case NETWORK_ONLINE:
1082 m_netPolicy = NETWORK_ONLINE;
1083 m_periodicMailboxNumbersRefresh->start();
1084 emit networkPolicyChanged();
1085 emit networkPolicyOnline();
1086 break;
1089 if (networkReconnected) {
1090 // We're connecting after being offline
1091 if (m_mailboxes->accessFetchStatus() != TreeItem::NONE) {
1092 // We should ask for an updated list of mailboxes
1093 // The main reason is that this happens after entering wrong password and going back online
1094 reloadMailboxList();
1096 } else if (m_netPolicy == NETWORK_ONLINE) {
1097 // The connection is online after some time in a different mode. Let's use this opportunity to request
1098 // updated message counts from all visible mailboxes.
1099 invalidateAllMessageCounts();
1103 void Model::handleSocketDisconnectedResponse(Parser *ptr, const Responses::SocketDisconnectedResponse *const resp)
1105 if (!accessParser(ptr).logoutCmd.isEmpty() || accessParser(ptr).connState == CONN_STATE_LOGOUT) {
1106 // If we're already scheduled for logout, don't treat connection errors as, well, errors.
1107 // This branch can be reached by e.g. user selecting offline after a network change, with logout
1108 // already on the fly.
1110 // But we still absolutely want to clean up and kill the connection/Parser anyway
1111 killParser(ptr, PARSER_KILL_EXPECTED);
1112 } else {
1113 logTrace(ptr->parserId(), Common::LOG_PARSE_ERROR, QString(), resp->message);
1114 changeConnectionState(ptr, CONN_STATE_LOGOUT);
1115 killParser(ptr, PARSER_KILL_EXPECTED);
1116 EMIT_LATER(this, networkError, Q_ARG(QString, resp->message));
1117 setNetworkPolicy(NETWORK_OFFLINE);
1121 void Model::handleParseErrorResponse(Imap::Parser *ptr, const Imap::Responses::ParseErrorResponse *const resp)
1123 Q_ASSERT(ptr);
1124 broadcastParseError(ptr->parserId(), resp->exceptionClass, resp->message, resp->line, resp->offset);
1125 killParser(ptr, PARSER_KILL_HARD);
1128 void Model::broadcastParseError(const uint parser, const QString &exceptionClass, const QString &errorMessage, const QByteArray &line, int position)
1130 emit logParserFatalError(parser, exceptionClass, errorMessage, line, position);
1131 QString details = (position == -1) ? QString() : QString(position, QLatin1Char(' ')) + QLatin1String("^ here");
1132 logTrace(parser, Common::LOG_PARSE_ERROR, exceptionClass, QStringLiteral("%1\n%2\n%3").arg(errorMessage, QString::fromUtf8(line), details));
1133 QString message;
1134 if (exceptionClass == QLatin1String("NotAnImapServerError")) {
1135 QString service;
1136 if (line.startsWith("+OK") || line.startsWith("-ERR")) {
1137 service = tr("<p>It appears that you are connecting to a POP3 server. That won't work here.</p>");
1138 } else if (line.startsWith("220 ") || line.startsWith("220-")) {
1139 service = tr("<p>It appears that you are connecting to an SMTP server. That won't work here.</p>");
1141 message = trUtf8("<h2>This is not an IMAP server</h2>"
1142 "%1"
1143 "<p>Please check your settings to make sure you are connecting to the IMAP service. "
1144 "A typical port number for IMAP is 143 or 993.</p>"
1145 "<p>The server said:</p>"
1146 "<pre>%2</pre>").arg(service, QString::fromUtf8(line));
1147 } else {
1148 message = trUtf8("<p>The IMAP server sent us a reply which we could not parse. "
1149 "This might either mean that there's a bug in Trojitá's code, or "
1150 "that the IMAP server you are connected to is broken. Please "
1151 "report this as a bug anyway. Here are the details:</p>"
1152 "<p><b>%1</b>: %2</p>"
1153 "<pre>%3\n%4</pre>"
1154 ).arg(exceptionClass, errorMessage, QString::fromUtf8(line), details);
1156 EMIT_LATER(this, imapError, Q_ARG(QString, message));
1157 setNetworkPolicy(NETWORK_OFFLINE);
1160 void Model::switchToMailbox(const QModelIndex &mbox)
1162 if (! mbox.isValid())
1163 return;
1165 if (m_netPolicy == NETWORK_OFFLINE)
1166 return;
1168 findTaskResponsibleFor(mbox);
1171 void Model::updateCapabilities(Parser *parser, const QStringList capabilities)
1173 Q_ASSERT(parser);
1174 QStringList uppercaseCaps;
1175 Q_FOREACH(const QString& str, capabilities) {
1176 QString cap = str.toUpper();
1177 if (m_capabilitiesBlacklist.contains(cap)) {
1178 logTrace(parser->parserId(), Common::LOG_OTHER, QStringLiteral("Model"), QStringLiteral("Ignoring capability \"%1\"").arg(cap));
1179 continue;
1181 uppercaseCaps << cap;
1183 accessParser(parser).capabilities = uppercaseCaps;
1184 accessParser(parser).capabilitiesFresh = true;
1185 if (uppercaseCaps.contains(QStringLiteral("LITERAL-"))) {
1186 parser->enableLiteralPlus(Parser::LiteralPlus::Minus);
1187 } else if (uppercaseCaps.contains(QStringLiteral("LITERAL+"))) {
1188 parser->enableLiteralPlus(Parser::LiteralPlus::Plus);
1189 } else {
1190 parser->enableLiteralPlus(Parser::LiteralPlus::Unsupported);
1193 for (QMap<Parser *,ParserState>::const_iterator it = m_parsers.constBegin(); it != m_parsers.constEnd(); ++it) {
1194 if (it->connState == CONN_STATE_LOGOUT) {
1195 // Skip all parsers which are currently stuck in LOGOUT
1196 continue;
1197 } else {
1198 // The CAPABILITIES were received by a first "usable" parser; let's treat this one as the authoritative one
1199 emit capabilitiesUpdated(uppercaseCaps);
1203 if (!uppercaseCaps.contains(QStringLiteral("IMAP4REV1"))) {
1204 changeConnectionState(parser, CONN_STATE_LOGOUT);
1205 accessParser(parser).logoutCmd = parser->logout();
1206 EMIT_LATER(this, imapError, Q_ARG(QString, tr("We aren't talking to an IMAP4 server")));
1207 setNetworkPolicy(NETWORK_OFFLINE);
1211 ImapTask *Model::setMessageFlags(const QModelIndexList &messages, const QString flag, const FlagsOperation marked)
1213 Q_ASSERT(!messages.isEmpty());
1214 Q_ASSERT(messages.front().model() == this);
1215 return m_taskFactory->createUpdateFlagsTask(this, messages, marked, QLatin1Char('(') + flag + QLatin1Char(')'));
1218 void Model::markMessagesDeleted(const QModelIndexList &messages, const FlagsOperation marked)
1220 this->setMessageFlags(messages, QStringLiteral("\\Deleted"), marked);
1223 void Model::markMailboxAsRead(const QModelIndex &mailbox)
1225 if (!mailbox.isValid())
1226 return;
1228 QModelIndex index;
1229 realTreeItem(mailbox, 0, &index);
1230 Q_ASSERT(index.isValid());
1231 Q_ASSERT(index.model() == this);
1232 Q_ASSERT(dynamic_cast<TreeItemMailbox*>(static_cast<TreeItem*>(index.internalPointer())));
1233 m_taskFactory->createUpdateFlagsOfAllMessagesTask(this, index, Imap::Mailbox::FLAG_ADD_SILENT, QStringLiteral("\\Seen"));
1236 void Model::markMessagesRead(const QModelIndexList &messages, const FlagsOperation marked)
1238 this->setMessageFlags(messages, QStringLiteral("\\Seen"), marked);
1241 void Model::copyMoveMessages(TreeItemMailbox *sourceMbox, const QString &destMailboxName, Imap::Uids uids, const CopyMoveOperation op)
1243 if (m_netPolicy == NETWORK_OFFLINE) {
1244 // FIXME: error signalling
1245 return;
1248 Q_ASSERT(sourceMbox);
1250 qSort(uids);
1252 QModelIndexList messages;
1253 Sequence seq;
1254 Q_FOREACH(TreeItemMessage* m, findMessagesByUids(sourceMbox, uids)) {
1255 messages << m->toIndex(this);
1256 seq.add(m->uid());
1258 m_taskFactory->createCopyMoveMessagesTask(this, messages, destMailboxName, op);
1261 /** @short Convert a list of UIDs to a list of pointers to the relevant message nodes */
1262 QList<TreeItemMessage *> Model::findMessagesByUids(const TreeItemMailbox *const mailbox, const Imap::Uids &uids)
1264 const TreeItemMsgList *const list = dynamic_cast<const TreeItemMsgList *const>(mailbox->m_children[0]);
1265 Q_ASSERT(list);
1266 QList<TreeItemMessage *> res;
1267 auto it = list->m_children.constBegin();
1268 uint lastUid = 0;
1269 Q_FOREACH(const uint uid, uids) {
1270 if (lastUid == uid) {
1271 // we have to filter out duplicates
1272 continue;
1274 lastUid = uid;
1275 it = Common::lowerBoundWithUnknownElements(it, list->m_children.constEnd(), uid, messageHasUidZero, uidComparator);
1276 if (it != list->m_children.end() && static_cast<TreeItemMessage *>(*it)->uid() == uid) {
1277 res << static_cast<TreeItemMessage *>(*it);
1278 } else {
1279 qDebug() << "Can't find UID" << uid;
1282 return res;
1285 /** @short Find a message with UID that matches the passed key, handling those with UID zero correctly
1287 If there's no such message, the next message with a valid UID is returned instead. If there are no such messages, the iterator can
1288 point to a message with UID zero or to the end of the list.
1290 TreeItemChildrenList::iterator Model::findMessageOrNextOneByUid(TreeItemMsgList *list, const uint uid)
1292 return Common::lowerBoundWithUnknownElements(list->m_children.begin(), list->m_children.end(), uid, messageHasUidZero, uidComparator);
1295 TreeItemMailbox *Model::findMailboxByName(const QString &name) const
1297 return findMailboxByName(name, m_mailboxes);
1300 TreeItemMailbox *Model::findMailboxByName(const QString &name, const TreeItemMailbox *const root) const
1302 Q_ASSERT(!root->m_children.isEmpty());
1303 // Names are sorted, so linear search is not required. On the ohterhand, the mailbox sizes are typically small enough
1304 // so that this shouldn't matter at all, and linear search is simple enough.
1305 for (int i = 1; i < root->m_children.size(); ++i) {
1306 TreeItemMailbox *mailbox = static_cast<TreeItemMailbox *>(root->m_children[i]);
1307 if (name == mailbox->mailbox())
1308 return mailbox;
1309 else if (name.startsWith(mailbox->mailbox() + mailbox->separator()))
1310 return findMailboxByName(name, mailbox);
1312 return 0;
1315 /** @short Find a parent mailbox for the specified name */
1316 TreeItemMailbox *Model::findParentMailboxByName(const QString &name) const
1318 TreeItemMailbox *root = m_mailboxes;
1319 while (true) {
1320 if (root->m_children.size() == 1) {
1321 break;
1323 bool found = false;
1324 for (int i = 1; !found && i < root->m_children.size(); ++i) {
1325 TreeItemMailbox *const item = dynamic_cast<TreeItemMailbox *>(root->m_children[i]);
1326 Q_ASSERT(item);
1327 if (name.startsWith(item->mailbox() + item->separator())) {
1328 root = item;
1329 found = true;
1332 if (!found) {
1333 return root;
1336 return root;
1340 void Model::expungeMailbox(const QModelIndex &mailbox)
1342 if (!mailbox.isValid())
1343 return;
1345 if (m_netPolicy == NETWORK_OFFLINE) {
1346 qDebug() << "Can't expunge while offline";
1347 return;
1350 m_taskFactory->createExpungeMailboxTask(this, mailbox);
1353 void Model::createMailbox(const QString &name, const AutoSubscription subscription)
1355 if (m_netPolicy == NETWORK_OFFLINE) {
1356 qDebug() << "Can't create mailboxes while offline";
1357 return;
1360 auto task = m_taskFactory->createCreateMailboxTask(this, name);
1361 if (subscription == AutoSubscription::SUBSCRIBE) {
1362 m_taskFactory->createSubscribeUnsubscribeTask(this, task, name, SUBSCRIBE);
1366 void Model::deleteMailbox(const QString &name)
1368 if (m_netPolicy == NETWORK_OFFLINE) {
1369 qDebug() << "Can't delete mailboxes while offline";
1370 return;
1373 m_taskFactory->createDeleteMailboxTask(this, name);
1376 void Model::subscribeMailbox(const QString &name)
1378 if (m_netPolicy == NETWORK_OFFLINE) {
1379 qDebug() << "Can't manage subscriptions while offline";
1380 return;
1383 TreeItemMailbox *mailbox = findMailboxByName(name);
1384 if (!mailbox) {
1385 qDebug() << "SUBSCRIBE: No such mailbox.";
1386 return;
1388 m_taskFactory->createSubscribeUnsubscribeTask(this, name, SUBSCRIBE);
1391 void Model::unsubscribeMailbox(const QString &name)
1393 if (m_netPolicy == NETWORK_OFFLINE) {
1394 qDebug() << "Can't manage subscriptions while offline";
1395 return;
1398 TreeItemMailbox *mailbox = findMailboxByName(name);
1399 if (!mailbox) {
1400 qDebug() << "UNSUBSCRIBE: No such mailbox.";
1401 return;
1403 m_taskFactory->createSubscribeUnsubscribeTask(this, name, UNSUBSCRIBE);
1406 void Model::saveUidMap(TreeItemMsgList *list)
1408 Imap::Uids seqToUid;
1409 seqToUid.reserve(list->m_children.size());
1410 auto end = list->m_children.constEnd();
1411 for (auto it = list->m_children.constBegin(); it != end; ++it)
1412 seqToUid << static_cast<TreeItemMessage *>(*it)->uid();
1413 cache()->setUidMapping(static_cast<TreeItemMailbox *>(list->parent())->mailbox(), seqToUid);
1417 TreeItem *Model::realTreeItem(QModelIndex index, const Model **whichModel, QModelIndex *translatedIndex)
1419 index = Imap::deproxifiedIndex(index);
1420 const Model *model = qobject_cast<const Model *>(index.model());
1421 Q_ASSERT(model);
1422 if (whichModel)
1423 *whichModel = model;
1424 if (translatedIndex)
1425 *translatedIndex = index;
1426 return static_cast<TreeItem *>(index.internalPointer());
1429 void Model::changeConnectionState(Parser *parser, ConnectionState state)
1431 accessParser(parser).connState = state;
1432 logTrace(parser->parserId(), Common::LOG_TASKS, QStringLiteral("conn"), connectionStateToString(state));
1433 emit connectionStateChanged(parser->parserId(), state);
1436 void Model::handleSocketStateChanged(Parser *parser, Imap::ConnectionState state)
1438 Q_ASSERT(parser);
1439 if (accessParser(parser).connState < state) {
1440 changeConnectionState(parser, state);
1444 void Model::killParser(Parser *parser, ParserKillingMethod method)
1446 if (method == PARSER_JUST_DELETE_LATER) {
1447 Q_ASSERT(accessParser(parser).parser == 0);
1448 Q_FOREACH(ImapTask *task, accessParser(parser).activeTasks) {
1449 task->deleteLater();
1451 parser->deleteLater();
1452 return;
1455 Q_FOREACH(ImapTask *task, accessParser(parser).activeTasks) {
1456 // FIXME: now this message sucks
1457 task->die(tr("The connection is being killed for unspecified reason"));
1460 parser->disconnect();
1461 Q_ASSERT(accessParser(parser).parser);
1462 accessParser(parser).parser = 0;
1463 switch (method) {
1464 case PARSER_KILL_EXPECTED:
1465 logTrace(parser->parserId(), Common::LOG_IO_WRITTEN, QString(), QStringLiteral("*** Connection closed."));
1466 return;
1467 case PARSER_KILL_HARD:
1468 logTrace(parser->parserId(), Common::LOG_IO_WRITTEN, QString(), QStringLiteral("*** Connection killed."));
1469 return;
1470 case PARSER_JUST_DELETE_LATER:
1471 // already handled
1472 return;
1474 Q_ASSERT(false);
1477 void Model::slotParserLineReceived(Parser *parser, const QByteArray &line)
1479 logTrace(parser->parserId(), Common::LOG_IO_READ, QString(), QString::fromUtf8(line));
1482 void Model::slotParserLineSent(Parser *parser, const QByteArray &line)
1484 logTrace(parser->parserId(), Common::LOG_IO_WRITTEN, QString(), QString::fromUtf8(line));
1487 void Model::setCache(std::shared_ptr<AbstractCache> cache)
1489 m_cache = cache;
1492 void Model::runReadyTasks()
1494 for (QMap<Parser *,ParserState>::iterator parserIt = m_parsers.begin(); parserIt != m_parsers.end(); ++parserIt) {
1495 bool runSomething = false;
1496 do {
1497 runSomething = false;
1498 // See responseReceived() for more details about why we do need to iterate over a copy here.
1499 // Basically, calls to ImapTask::perform could invalidate our precious iterators.
1500 QList<ImapTask *> origList = parserIt->activeTasks;
1501 QList<ImapTask *> deletedList;
1502 QList<ImapTask *>::const_iterator taskEnd = origList.constEnd();
1503 for (QList<ImapTask *>::const_iterator taskIt = origList.constBegin(); taskIt != taskEnd; ++taskIt) {
1504 ImapTask *task = *taskIt;
1505 if (task->isReadyToRun()) {
1506 task->perform();
1507 runSomething = true;
1509 if (task->isFinished()) {
1510 deletedList << task;
1513 removeDeletedTasks(deletedList, parserIt->activeTasks);
1514 #ifdef TROJITA_DEBUG_TASK_TREE
1515 if (!deletedList.isEmpty())
1516 checkTaskTreeConsistency();
1517 #endif
1518 } while (runSomething);
1522 void Model::removeDeletedTasks(const QList<ImapTask *> &deletedTasks, QList<ImapTask *> &activeTasks)
1524 // Remove the finished commands
1525 for (QList<ImapTask *>::const_iterator deletedIt = deletedTasks.begin(); deletedIt != deletedTasks.end(); ++deletedIt) {
1526 (*deletedIt)->deleteLater();
1527 activeTasks.removeOne(*deletedIt);
1528 // It isn't destroyed yet, but should be removed from the model nonetheless
1529 m_taskModel->slotSomeTaskDestroyed();
1533 KeepMailboxOpenTask *Model::findTaskResponsibleFor(const QModelIndex &mailbox)
1535 Q_ASSERT(mailbox.isValid());
1536 QModelIndex translatedIndex;
1537 TreeItemMailbox *mailboxPtr = dynamic_cast<TreeItemMailbox *>(realTreeItem(mailbox, 0, &translatedIndex));
1538 return findTaskResponsibleFor(mailboxPtr);
1541 KeepMailboxOpenTask *Model::findTaskResponsibleFor(TreeItemMailbox *mailboxPtr)
1543 Q_ASSERT(mailboxPtr);
1544 bool canCreateParallelConn = m_parsers.isEmpty(); // FIXME: multiple connections
1546 if (mailboxPtr->maintainingTask) {
1547 // The requested mailbox already has the maintaining task associated
1548 if (accessParser(mailboxPtr->maintainingTask->parser).connState == CONN_STATE_LOGOUT) {
1549 // The connection is currently getting closed, so we have to create another one
1550 return m_taskFactory->createKeepMailboxOpenTask(this, mailboxPtr->toIndex(this), 0);
1551 } else {
1552 // it's usable as-is
1553 return mailboxPtr->maintainingTask;
1555 } else if (canCreateParallelConn) {
1556 // The mailbox is not being maintained, but we can create a new connection
1557 return m_taskFactory->createKeepMailboxOpenTask(this, mailboxPtr->toIndex(this), 0);
1558 } else {
1559 // Too bad, we have to re-use an existing parser. That will probably lead to
1560 // stealing it from some mailbox, but there's no other way.
1561 Q_ASSERT(!m_parsers.isEmpty());
1563 for (QMap<Parser *,ParserState>::const_iterator it = m_parsers.constBegin(); it != m_parsers.constEnd(); ++it) {
1564 if (it->connState == CONN_STATE_LOGOUT) {
1565 // this one is not usable
1566 continue;
1568 return m_taskFactory->createKeepMailboxOpenTask(this, mailboxPtr->toIndex(this), it.key());
1570 // At this point, we have no other choice than to create a new connection
1571 return m_taskFactory->createKeepMailboxOpenTask(this, mailboxPtr->toIndex(this), 0);
1575 void Model::genericHandleFetch(TreeItemMailbox *mailbox, const Imap::Responses::Fetch *const resp)
1577 Q_ASSERT(mailbox);
1578 QList<TreeItemPart *> changedParts;
1579 TreeItemMessage *changedMessage = 0;
1580 mailbox->handleFetchResponse(this, *resp, changedParts, changedMessage, false);
1581 if (! changedParts.isEmpty()) {
1582 Q_FOREACH(TreeItemPart* part, changedParts) {
1583 QModelIndex index = part->toIndex(this);
1584 emit dataChanged(index, index);
1587 if (changedMessage) {
1588 QModelIndex index = changedMessage->toIndex(this);
1589 emit dataChanged(index, index);
1590 emitMessageCountChanged(mailbox);
1594 QModelIndex Model::findMailboxForItems(const QModelIndexList &items)
1596 TreeItemMailbox *mailbox = 0;
1597 Q_FOREACH(const QModelIndex& index, items) {
1598 TreeItemMailbox *currentMailbox = 0;
1599 Q_ASSERT(index.model() == this);
1601 TreeItem *item = static_cast<TreeItem *>(index.internalPointer());
1602 Q_ASSERT(item);
1604 if ((currentMailbox = dynamic_cast<TreeItemMailbox *>(item))) {
1605 // yes, that's an assignment, not a comparison
1607 // This case is OK
1608 } else {
1609 // TreeItemMessage and TreeItemPart have to walk the tree, which is why they are lumped together in this branch
1610 TreeItemMessage *message = dynamic_cast<TreeItemMessage *>(item);
1611 if (!message) {
1612 if (TreeItemPart *part = dynamic_cast<TreeItemPart *>(item)) {
1613 message = part->message();
1614 } else {
1615 throw CantHappen("findMailboxForItems() called on strange items");
1618 Q_ASSERT(message);
1619 TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(message->parent());
1620 Q_ASSERT(list);
1621 currentMailbox = dynamic_cast<TreeItemMailbox *>(list->parent());
1624 Q_ASSERT(currentMailbox);
1625 if (!mailbox) {
1626 mailbox = currentMailbox;
1627 } else if (mailbox != currentMailbox) {
1628 throw CantHappen("Messages from several mailboxes");
1631 return mailbox->toIndex(this);
1634 void Model::slotTasksChanged()
1636 dumpModelContents(m_taskModel);
1639 void Model::slotTaskDying(QObject *obj)
1641 std::for_each(m_parsers.begin(), m_parsers.end(), [obj](ParserState &state) {
1642 state.activeTasks.removeOne(reinterpret_cast<ImapTask*>(obj));
1644 m_taskModel->slotSomeTaskDestroyed();
1647 TreeItemMailbox *Model::mailboxForSomeItem(QModelIndex index)
1649 TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(index.internalPointer()));
1650 while (index.isValid() && ! mailbox) {
1651 index = index.parent();
1652 mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(index.internalPointer()));
1654 return mailbox;
1657 ParserState &Model::accessParser(Parser *parser)
1659 Q_ASSERT(m_parsers.contains(parser));
1660 return m_parsers[ parser ];
1663 void Model::releaseMessageData(const QModelIndex &message)
1665 if (! message.isValid())
1666 return;
1668 const Model *whichModel = 0;
1669 QModelIndex realMessage;
1670 realTreeItem(message, &whichModel, &realMessage);
1671 Q_ASSERT(whichModel == this);
1673 TreeItemMessage *msg = dynamic_cast<TreeItemMessage *>(static_cast<TreeItem *>(realMessage.internalPointer()));
1674 if (! msg)
1675 return;
1677 msg->setFetchStatus(TreeItem::NONE);
1679 #ifndef XTUPLE_CONNECT
1680 beginRemoveRows(realMessage, 0, msg->m_children.size() - 1);
1681 #endif
1682 if (msg->data()->partHeader()) {
1683 msg->data()->partHeader()->silentlyReleaseMemoryRecursive();
1684 msg->data()->setPartHeader(nullptr);
1686 if (msg->data()->partText()) {
1687 msg->data()->partText()->silentlyReleaseMemoryRecursive();
1688 msg->data()->setPartText(nullptr);
1690 delete msg->m_data;
1691 msg->m_data = 0;
1692 Q_FOREACH(TreeItem *item, msg->m_children) {
1693 TreeItemPart *part = dynamic_cast<TreeItemPart *>(item);
1694 Q_ASSERT(part);
1695 part->silentlyReleaseMemoryRecursive();
1696 delete part;
1698 msg->m_children.clear();
1699 #ifndef XTUPLE_CONNECT
1700 endRemoveRows();
1701 emit dataChanged(realMessage, realMessage);
1702 #endif
1705 QStringList Model::capabilities() const
1707 if (m_parsers.isEmpty())
1708 return QStringList();
1710 if (m_parsers.constBegin()->capabilitiesFresh)
1711 return m_parsers.constBegin()->capabilities;
1713 return QStringList();
1716 void Model::logTrace(uint parserId, const Common::LogKind kind, const QString &source, const QString &message)
1718 Common::LogMessage m(QDateTime::currentDateTime(), kind, source, message, 0);
1719 emit logged(parserId, m);
1722 /** @short Overloaded version which accepts a QModelIndex of an item which is somehow "related" to the logged message
1724 The relevantIndex argument is used for finding out what parser to send the message to.
1726 void Model::logTrace(const QModelIndex &relevantIndex, const Common::LogKind kind, const QString &source, const QString &message)
1728 Q_ASSERT(relevantIndex.isValid());
1729 QModelIndex translatedIndex;
1730 realTreeItem(relevantIndex, 0, &translatedIndex);
1732 // It appears that it's OK to use 0 here; the attached loggers apparently deal with random parsers appearing just OK
1733 uint parserId = 0;
1735 if (translatedIndex.isValid()) {
1736 Q_ASSERT(translatedIndex.model() == this);
1737 QModelIndex mailboxIndex = findMailboxForItems(QModelIndexList() << translatedIndex);
1738 Q_ASSERT(mailboxIndex.isValid());
1739 TreeItemMailbox *mailboxPtr = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
1740 Q_ASSERT(mailboxPtr);
1741 if (mailboxPtr->maintainingTask) {
1742 parserId = mailboxPtr->maintainingTask->parser->parserId();
1746 logTrace(parserId, kind, source, message);
1749 QAbstractItemModel *Model::taskModel() const
1751 return m_taskModel;
1754 QMap<QByteArray,QByteArray> Model::serverId() const
1756 return m_idResult;
1759 /** @short Handle explicit sharing and case mapping for message flags
1761 This function will try to minimize the amount of QString instances used for storage of individual message flags via Qt's implicit
1762 sharing that is built into QString.
1764 At the same time, some well-known flags are converted to their "canonical" form (like \\SEEN -> \\Seen etc).
1766 QStringList Model::normalizeFlags(const QStringList &source) const
1768 QStringList res;
1769 res.reserve(source.size());
1770 for (QStringList::const_iterator flag = source.constBegin(); flag != source.constEnd(); ++flag) {
1772 // At first, perform a case-insensitive lookup in the (rather short) list of known special flags
1773 // Only call the toLower for flags which could possibly be in that mapping. Looking at the first letter is
1774 // a good approximation.
1775 if (!flag->isEmpty() && ((*flag)[0] == QLatin1Char('\\') || (*flag)[0] == QLatin1Char('$'))) {
1776 QString lowerCase = flag->toLower();
1777 QHash<QString,QString>::const_iterator known = FlagNames::toCanonical.constFind(lowerCase);
1778 if (known != FlagNames::toCanonical.constEnd()) {
1779 res.append(*known);
1780 continue;
1784 // If it isn't a special flag, just check whether it's been encountered already
1785 QSet<QString>::const_iterator it = m_flagLiterals.constFind(*flag);
1786 if (it == m_flagLiterals.constEnd()) {
1787 // Not in cache, so add it and return an implicitly shared copy
1788 m_flagLiterals.insert(*flag);
1789 res.append(*flag);
1790 } else {
1791 // It's in the cache already, se let's QString share the data
1792 res.append(*it);
1795 // Always sort the flags when performing normalization to obtain reasonable results and be ready for possible future
1796 // deduplication of the actual QLists
1797 res.sort();
1798 return res;
1801 /** @short Set the IMAP username */
1802 void Model::setImapUser(const QString &imapUser)
1804 m_imapUser = imapUser;
1807 /** @short Username to use for login */
1808 QString Model::imapUser() const
1810 return m_imapUser;
1813 /** @short Set the password that the user wants to use */
1814 void Model::setImapPassword(const QString &password)
1816 m_imapPassword = password;
1817 m_hasImapPassword = PasswordAvailability::AVAILABLE;
1818 informTasksAboutNewPassword();
1821 /** @short Return the user's password, if cached */
1822 QString Model::imapPassword() const
1824 return m_imapPassword;
1827 /** @short Indicate that the user doesn't want to provide her password */
1828 void Model::unsetImapPassword()
1830 m_imapPassword.clear();
1831 m_hasImapPassword = PasswordAvailability::NOT_REQUESTED;
1832 informTasksAboutNewPassword();
1835 QString Model::imapAuthError() const
1837 return m_imapAuthError;
1840 void Model::setImapAuthError(const QString &error)
1842 if (m_imapAuthError == error)
1843 return;
1844 m_imapAuthError = error;
1845 emit imapAuthErrorChanged(error);
1848 /** @short Tell all tasks which want to know about the availability of a password */
1849 void Model::informTasksAboutNewPassword()
1851 Q_FOREACH(const ParserState &p, m_parsers) {
1852 Q_FOREACH(ImapTask *task, p.activeTasks) {
1853 OpenConnectionTask *openTask = dynamic_cast<OpenConnectionTask *>(task);
1854 if (!openTask)
1855 continue;
1856 openTask->authCredentialsNowAvailable();
1861 /** @short Forward a policy decision about accepting or rejecting a SSL state */
1862 void Model::setSslPolicy(const QList<QSslCertificate> &sslChain, const QList<QSslError> &sslErrors, bool proceed)
1864 if (proceed) {
1865 // Only remember positive values; there is no point in blocking any further connections until settings reload
1866 m_sslErrorPolicy.prepend(qMakePair(qMakePair(sslChain, sslErrors), proceed));
1868 Q_FOREACH(const ParserState &p, m_parsers) {
1869 Q_FOREACH(ImapTask *task, p.activeTasks) {
1870 OpenConnectionTask *openTask = dynamic_cast<OpenConnectionTask *>(task);
1871 if (!openTask)
1872 continue;
1873 if (openTask->sslCertificateChain() == sslChain && openTask->sslErrors() == sslErrors) {
1874 openTask->sslConnectionPolicyDecided(proceed);
1880 void Model::processSslErrors(OpenConnectionTask *task)
1882 // Qt doesn't define either operator< or a qHash specialization for QList<QSslError> (what a surprise),
1883 // so we use a plain old QList. Given that there will be at most one different QList<QSslError> sequence for
1884 // each connection attempt (and more realistically, for each server at all), this O(n) complexity shall not matter
1885 // at all.
1886 QList<QPair<QPair<QList<QSslCertificate>, QList<QSslError> >, bool> >::const_iterator it = m_sslErrorPolicy.constBegin();
1887 while (it != m_sslErrorPolicy.constEnd()) {
1888 if (it->first.first == task->sslCertificateChain() && it->first.second == task->sslErrors()) {
1889 task->sslConnectionPolicyDecided(it->second);
1890 return;
1892 ++it;
1894 EMIT_LATER(this, needsSslDecision, Q_ARG(QList<QSslCertificate>, task->sslCertificateChain()), Q_ARG(QList<QSslError>, task->sslErrors()));
1897 QModelIndex Model::messageIndexByUid(const QString &mailboxName, const uint uid)
1899 TreeItemMailbox *mailbox = findMailboxByName(mailboxName);
1900 Q_ASSERT(mailbox);
1901 QList<TreeItemMessage*> messages = findMessagesByUids(mailbox, Imap::Uids() << uid);
1902 if (messages.isEmpty()) {
1903 return QModelIndex();
1904 } else {
1905 Q_ASSERT(messages.size() == 1);
1906 return messages.front()->toIndex(this);
1910 /** @short Forget any cached data about number of messages in all mailboxes */
1911 void Model::invalidateAllMessageCounts()
1913 QList<TreeItemMailbox*> queue;
1914 queue.append(m_mailboxes);
1915 while (!queue.isEmpty()) {
1916 TreeItemMailbox *head = queue.takeFirst();
1917 // ignore first child, the TreeItemMsgList
1918 for (auto it = head->m_children.constBegin() + 1; it != head->m_children.constEnd(); ++it) {
1919 queue.append(static_cast<TreeItemMailbox*>(*it));
1921 TreeItemMsgList *list = dynamic_cast<TreeItemMsgList*>(head->m_children[0]);
1923 if (list->m_numberFetchingStatus == TreeItem::DONE && !head->maintainingTask) {
1924 // Ask only for data which were previously available
1925 // Also don't mess with a mailbox which is already being kept up-to-date because it's selected.
1926 list->m_numberFetchingStatus = TreeItem::NONE;
1927 emitMessageCountChanged(head);
1932 AppendTask *Model::appendIntoMailbox(const QString &mailbox, const QByteArray &rawMessageData, const QStringList &flags,
1933 const QDateTime &timestamp)
1935 return m_taskFactory->createAppendTask(this, mailbox, rawMessageData, flags, timestamp);
1938 AppendTask *Model::appendIntoMailbox(const QString &mailbox, const QList<CatenatePair> &data, const QStringList &flags,
1939 const QDateTime &timestamp)
1941 return m_taskFactory->createAppendTask(this, mailbox, data, flags, timestamp);
1944 GenUrlAuthTask *Model::generateUrlAuthForMessage(const QString &host, const QString &user, const QString &mailbox,
1945 const uint uidValidity, const uint uid, const QString &part, const QString &access)
1947 return m_taskFactory->createGenUrlAuthTask(this, host, user, mailbox, uidValidity, uid, part, access);
1950 UidSubmitTask *Model::sendMailViaUidSubmit(const QString &mailbox, const uint uidValidity, const uint uid,
1951 const UidSubmitOptionsList &options)
1953 return m_taskFactory->createUidSubmitTask(this, mailbox, uidValidity, uid, options);
1956 #ifdef TROJITA_DEBUG_TASK_TREE
1957 #define TROJITA_DEBUG_TASK_TREE_VERBOSE
1958 void Model::checkTaskTreeConsistency()
1960 for (QMap<Parser *,ParserState>::const_iterator parserIt = m_parsers.constBegin(); parserIt != m_parsers.constEnd(); ++parserIt) {
1961 #ifdef TROJITA_DEBUG_TASK_TREE_VERBOSE
1962 qDebug() << "\nParser" << parserIt.key() << "; all active tasks:";
1963 Q_FOREACH(ImapTask *activeTask, parserIt.value().activeTasks) {
1964 qDebug() << ' ' << activeTask << activeTask->debugIdentification() << activeTask->parser;
1966 #endif
1967 Q_FOREACH(ImapTask *activeTask, parserIt.value().activeTasks) {
1968 #ifdef TROJITA_DEBUG_TASK_TREE_VERBOSE
1969 qDebug() << "Active task" << activeTask << activeTask->debugIdentification() << activeTask->parser;
1970 #endif
1971 Q_ASSERT(activeTask->parser == parserIt.key());
1972 Q_ASSERT(!activeTask->parentTask);
1973 checkDependentTasksConsistency(parserIt.key(), activeTask, 0, 0);
1976 // Make sure that no task is present twice in here
1977 QList<ImapTask*> taskQueue = parserIt.value().activeTasks;
1978 for (int i = 0; i < taskQueue.size(); ++i) {
1979 Q_FOREACH(ImapTask *yetAnotherTask, taskQueue[i]->dependentTasks) {
1980 Q_ASSERT(!taskQueue.contains(yetAnotherTask));
1981 taskQueue.push_back(yetAnotherTask);
1987 void Model::checkDependentTasksConsistency(Parser *parser, ImapTask *task, ImapTask *expectedParentTask, int depth)
1989 #ifdef TROJITA_DEBUG_TASK_TREE_VERBOSE
1990 QByteArray prefix;
1991 prefix.fill(' ', depth);
1992 qDebug() << prefix.constData() << "Checking" << task << task->debugIdentification();
1993 #endif
1994 Q_ASSERT(parser);
1995 Q_ASSERT(!task->parser || task->parser == parser);
1996 Q_ASSERT(task->parentTask == expectedParentTask);
1997 if (task->parentTask) {
1998 Q_ASSERT(task->parentTask->dependentTasks.contains(task));
1999 if (task->parentTask->parentTask) {
2000 Q_ASSERT(task->parentTask->parentTask->dependentTasks.contains(task->parentTask));
2001 } else {
2002 Q_ASSERT(task->parentTask->parser);
2003 Q_ASSERT(accessParser(task->parentTask->parser).activeTasks.contains(task->parentTask));
2005 } else {
2006 Q_ASSERT(accessParser(parser).activeTasks.contains(task));
2009 Q_FOREACH(ImapTask *childTask, task->dependentTasks) {
2010 checkDependentTasksConsistency(parser, childTask, task, depth + 1);
2013 #endif
2015 void Model::setCapabilitiesBlacklist(const QStringList &blacklist)
2017 m_capabilitiesBlacklist = blacklist;
2020 bool Model::isCatenateSupported() const
2022 return capabilities().contains(QStringLiteral("CATENATE"));
2025 bool Model::isGenUrlAuthSupported() const
2027 return capabilities().contains(QStringLiteral("URLAUTH"));
2030 bool Model::isImapSubmissionSupported() const
2032 QStringList caps = capabilities();
2033 return caps.contains(QStringLiteral("UIDPLUS")) && caps.contains(QStringLiteral("X-DRAFT-I01-SENDMAIL"));
2036 void Model::setNumberRefreshInterval(const int interval)
2038 if (interval == m_periodicMailboxNumbersRefresh->interval())
2039 return; // QTimer does not check idempotency
2040 m_periodicMailboxNumbersRefresh->start(interval * 1000);