File indexing completed on 2024-11-24 04:53:09
0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net> 0002 0003 This file is part of the Trojita Qt IMAP e-mail client, 0004 http://trojita.flaska.net/ 0005 0006 This program is free software; you can redistribute it and/or 0007 modify it under the terms of the GNU General Public License as 0008 published by the Free Software Foundation; either version 2 of 0009 the License or (at your option) version 3 or any later version 0010 accepted by the membership of KDE e.V. (or its successor approved 0011 by the membership of KDE e.V.), which shall act as a proxy 0012 defined in Section 14 of version 3 of the license. 0013 0014 This program is distributed in the hope that it will be useful, 0015 but WITHOUT ANY WARRANTY; without even the implied warranty of 0016 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0017 GNU General Public License for more details. 0018 0019 You should have received a copy of the GNU General Public License 0020 along with this program. If not, see <http://www.gnu.org/licenses/>. 0021 */ 0022 0023 #include <QDebug> 0024 #include <QFile> 0025 #include <QFileInfo> 0026 #include <QMimeData> 0027 #include <QMimeDatabase> 0028 #include "Imap/Model/DragAndDrop.h" 0029 #include "Imap/Model/ItemRoles.h" 0030 #include "Imap/Model/MailboxModel.h" 0031 #include "Imap/Model/MailboxTree.h" 0032 #include "Imap/Model/SpecialFlagNames.h" 0033 0034 namespace Imap 0035 { 0036 namespace Mailbox 0037 { 0038 0039 /** @short Does this URL point to an Internet e-mail message, according to the MIME type? */ 0040 static bool isFileWithMimeMessage(const QUrl &url) 0041 { 0042 QMimeDatabase mimeDb; // the docs say this is cheap to construct 0043 return url.isLocalFile() && mimeDb.mimeTypeForFile(url.path()).inherits(QStringLiteral("message/rfc822")); 0044 } 0045 0046 MailboxModel::MailboxModel(QObject *parent, Model *model): QAbstractProxyModel(parent) 0047 { 0048 setSourceModel(model); 0049 0050 // FIXME: will need to be expanded when Model supports more signals... 0051 connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &MailboxModel::handleModelAboutToBeReset); 0052 connect(model, &QAbstractItemModel::modelReset, this, &MailboxModel::handleModelReset); 0053 connect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, &QAbstractItemModel::layoutAboutToBeChanged); 0054 connect(model, &QAbstractItemModel::layoutChanged, this, &QAbstractItemModel::layoutChanged); 0055 connect(model, &QAbstractItemModel::dataChanged, this, &MailboxModel::handleDataChanged); 0056 connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MailboxModel::handleRowsAboutToBeRemoved); 0057 connect(model, &QAbstractItemModel::rowsRemoved, this, &MailboxModel::handleRowsRemoved); 0058 connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this, &MailboxModel::handleRowsAboutToBeInserted); 0059 connect(model, &QAbstractItemModel::rowsInserted, this, &MailboxModel::handleRowsInserted); 0060 connect(model, &Model::messageCountPossiblyChanged, this, &MailboxModel::handleMessageCountPossiblyChanged); 0061 } 0062 0063 QHash<int, QByteArray> MailboxModel::roleNames() const 0064 { 0065 static QHash<int, QByteArray> roleNames; 0066 if (roleNames.isEmpty()) { 0067 roleNames[RoleIsFetched] = "isFetched"; 0068 roleNames[RoleShortMailboxName] = "shortMailboxName"; 0069 roleNames[RoleMailboxName] = "mailboxName"; 0070 roleNames[RoleMailboxSeparator] = "mailboxSeparator"; 0071 roleNames[RoleMailboxHasChildMailboxes] = "mailboxHasChildMailboxes"; 0072 roleNames[RoleMailboxIsINBOX] = "mailboxIsINBOX"; 0073 roleNames[RoleMailboxIsSelectable] = "mailboxIsSelectable"; 0074 roleNames[RoleMailboxNumbersFetched] = "mailboxNumbersFetched"; 0075 roleNames[RoleTotalMessageCount] = "totalMessageCount"; 0076 roleNames[RoleUnreadMessageCount] = "unreadMessageCount"; 0077 roleNames[RoleRecentMessageCount] = "recentMessageCount"; 0078 roleNames[RoleMailboxItemsAreLoading] = "mailboxItemsAreLoading"; 0079 } 0080 return roleNames; 0081 } 0082 0083 void MailboxModel::handleModelAboutToBeReset() 0084 { 0085 beginResetModel(); 0086 } 0087 0088 void MailboxModel::handleModelReset() 0089 { 0090 endResetModel(); 0091 } 0092 0093 bool MailboxModel::hasChildren(const QModelIndex &parent) const 0094 { 0095 if (parent.isValid() && parent.column() != 0) 0096 return false; 0097 0098 QModelIndex index = mapToSource(parent); 0099 0100 TreeItemMailbox *mbox = dynamic_cast<TreeItemMailbox *>( 0101 static_cast<TreeItem *>( 0102 index.internalPointer() 0103 )); 0104 return mbox ? 0105 mbox->hasChildMailboxes(static_cast<Model *>(sourceModel())) : 0106 sourceModel()->hasChildren(index); 0107 } 0108 0109 void MailboxModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) 0110 { 0111 QModelIndex first = mapFromSource(topLeft); 0112 QModelIndex second = mapFromSource(bottomRight); 0113 0114 if (! first.isValid() || ! second.isValid()) { 0115 // It's something completely alien... 0116 return; 0117 } 0118 0119 if (first.parent() == second.parent() && first.column() == second.column()) { 0120 emit dataChanged(first, second); 0121 } else { 0122 // FIXME: batched updates aren't used yet 0123 Q_ASSERT(false); 0124 return; 0125 } 0126 } 0127 0128 QModelIndex MailboxModel::index(int row, int column, const QModelIndex &parent) const 0129 { 0130 if (row < 0 || column != 0) 0131 return QModelIndex(); 0132 0133 if (parent.column() != 0 && parent.column() != -1) 0134 return QModelIndex(); 0135 0136 QModelIndex translatedParent = mapToSource(parent); 0137 0138 if (row < sourceModel()->rowCount(translatedParent) - 1) { 0139 void *ptr = sourceModel()->index(row + 1, 0, translatedParent).internalPointer(); 0140 Q_ASSERT(ptr); 0141 return createIndex(row, column, ptr); 0142 } else { 0143 return QModelIndex(); 0144 } 0145 } 0146 0147 QModelIndex MailboxModel::parent(const QModelIndex &index) const 0148 { 0149 return mapFromSource(mapToSource(index).parent()); 0150 } 0151 0152 int MailboxModel::rowCount(const QModelIndex &parent) const 0153 { 0154 if (parent.column() != 0 && parent.column() != -1) 0155 return 0; 0156 int res = sourceModel()->rowCount(mapToSource(parent)); 0157 if (res > 0) 0158 --res; 0159 return res; 0160 } 0161 0162 int MailboxModel::columnCount(const QModelIndex &parent) const 0163 { 0164 return parent.column() == 0 || parent.column() == -1 ? 1 : 0; 0165 } 0166 0167 QModelIndex MailboxModel::mapToSource(const QModelIndex &proxyIndex) const 0168 { 0169 int row = proxyIndex.row(); 0170 if (row < 0 || proxyIndex.column() != 0) 0171 return QModelIndex(); 0172 ++row; 0173 return static_cast<Imap::Mailbox::Model *>(sourceModel())->createIndex(row, 0, proxyIndex.internalPointer()); 0174 } 0175 0176 QModelIndex MailboxModel::mapFromSource(const QModelIndex &sourceIndex) const 0177 { 0178 if (!sourceIndex.isValid()) 0179 return QModelIndex(); 0180 0181 if (! dynamic_cast<Imap::Mailbox::TreeItemMailbox *>( 0182 static_cast<Imap::Mailbox::TreeItem *>(sourceIndex.internalPointer()))) 0183 return QModelIndex(); 0184 0185 int row = sourceIndex.row(); 0186 if (row == 0) 0187 return QModelIndex(); 0188 if (row > 0) 0189 --row; 0190 if (sourceIndex.column() != 0) 0191 return QModelIndex(); 0192 0193 return createIndex(row, 0, sourceIndex.internalPointer()); 0194 } 0195 0196 QVariant MailboxModel::data(const QModelIndex &proxyIndex, int role) const 0197 { 0198 if (! proxyIndex.isValid() || proxyIndex.model() != this) 0199 return QVariant(); 0200 0201 if (proxyIndex.column() != 0) 0202 return QVariant(); 0203 0204 TreeItemMailbox *mbox = dynamic_cast<TreeItemMailbox *>( 0205 static_cast<TreeItem *>(proxyIndex.internalPointer()) 0206 ); 0207 Q_ASSERT(mbox); 0208 if (role > RoleBase && role < RoleInvalidLastOne) 0209 return mbox->data(static_cast<Imap::Mailbox::Model *>(sourceModel()), role); 0210 else 0211 return QAbstractProxyModel::data(createIndex(proxyIndex.row(), 0, proxyIndex.internalPointer()), role); 0212 } 0213 0214 void MailboxModel::handleMessageCountPossiblyChanged(const QModelIndex &mailbox) 0215 { 0216 QModelIndex translated = mapFromSource(mailbox); 0217 if (translated.isValid()) { 0218 emit dataChanged(translated, translated); 0219 } 0220 } 0221 0222 Qt::ItemFlags MailboxModel::flags(const QModelIndex &index) const 0223 { 0224 if (! index.isValid()) 0225 return QAbstractProxyModel::flags(index); 0226 0227 TreeItemMailbox *mbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(index.internalPointer())); 0228 Q_ASSERT(mbox); 0229 0230 Qt::ItemFlags res = QAbstractProxyModel::flags(index); 0231 if (!mbox->isSelectable()) { 0232 res &= ~Qt::ItemIsSelectable; 0233 res |= Qt::ItemIsEnabled; 0234 } 0235 if (static_cast<Model *>(sourceModel())->isNetworkAvailable()) { 0236 res |= Qt::ItemIsDropEnabled; 0237 } 0238 return res; 0239 } 0240 0241 Qt::DropActions MailboxModel::supportedDropActions() const 0242 { 0243 return Qt::CopyAction | Qt::MoveAction; 0244 } 0245 0246 QStringList MailboxModel::mimeTypes() const 0247 { 0248 return QStringList() << MimeTypes::xTrojitaMessageList; 0249 } 0250 0251 bool MailboxModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const 0252 { 0253 // At first, check for dropping of URLs. We have to handle this with a priority because otherwise mimeTypes() gets called, 0254 // and we deliberately do not list our messages as URLs because our URLs are proprietary. 0255 const auto urls = data->urls(); 0256 if (std::any_of(urls.begin(), urls.end(), isFileWithMimeMessage)) { 0257 return true; 0258 } 0259 0260 // We cannot delegate this to QAbstractProxyModel::canDropMimeData because that code delegates the decision 0261 // to the *source* model. That's bad, because our source model doesn't know anything about drag-and-drops 0262 // or MIME types. 0263 // However, calling the default implementation *at this level* of proxy chain makes sure that this proxy's 0264 // mimeTypes() and supportedDropActions() gets consulted, which is the correct thing to do. 0265 return QAbstractItemModel::canDropMimeData(data, action, row, column, parent); 0266 } 0267 0268 bool MailboxModel::dropMimeData(const QMimeData *data, Qt::DropAction action, 0269 int row, int column, const QModelIndex &parent) 0270 { 0271 Q_UNUSED(row); Q_UNUSED(column); 0272 if (action != Qt::CopyAction && action != Qt::MoveAction) 0273 return false; 0274 0275 if (! parent.isValid()) 0276 return false; 0277 0278 if (! static_cast<Model *>(sourceModel())->isNetworkAvailable()) 0279 return false; 0280 0281 TreeItemMailbox *target = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer())); 0282 Q_ASSERT(target); 0283 0284 if (! target->isSelectable()) 0285 return false; 0286 0287 if (data->hasFormat(MimeTypes::xTrojitaMessageList)) { 0288 return dropTrojitaMessageList(target->mailbox(), action, data->data(MimeTypes::xTrojitaMessageList)); 0289 } else if (data->hasUrls()) { 0290 return dropFileUrlList(target->mailbox(), data->urls()); 0291 } else { 0292 return false; 0293 } 0294 } 0295 0296 bool MailboxModel::dropTrojitaMessageList(const QString &mailboxName, const Qt::DropAction action, const QByteArray &encodedData) 0297 { 0298 QDataStream stream(&const_cast<QByteArray &>(encodedData), QIODevice::ReadOnly); 0299 0300 Q_ASSERT(!stream.atEnd()); 0301 QString origMboxName; 0302 stream >> origMboxName; 0303 TreeItemMailbox *origMbox = static_cast<Model *>(sourceModel())->findMailboxByName(origMboxName); 0304 if (! origMbox) { 0305 qDebug() << "Can't find original mailbox when performing a drag&drop on messages"; 0306 return false; 0307 } 0308 0309 uint uidValidity; 0310 stream >> uidValidity; 0311 if (uidValidity != origMbox->syncState.uidValidity()) { 0312 qDebug() << "UID validity for original mailbox got changed, can't copy messages"; 0313 return false; 0314 } 0315 0316 Imap::Uids uids; 0317 stream >> uids; 0318 0319 static_cast<Model *>(sourceModel())->copyMoveMessages(origMbox, mailboxName, uids, 0320 (action == Qt::MoveAction) ? MOVE : COPY); 0321 return true; 0322 } 0323 0324 bool MailboxModel::dropFileUrlList(const QString &mailboxName, QList<QUrl> files) 0325 { 0326 bool ok = false; 0327 0328 files.erase(std::remove_if(files.begin(), files.end(), [](const auto &file) { return !isFileWithMimeMessage(file); }), files.end()); 0329 std::for_each(files.begin(), files.end(), [this, mailboxName, &ok](const QUrl &url){ 0330 QFile f(url.path()); 0331 if (!f.open(QIODevice::ReadOnly)) 0332 return; 0333 0334 auto content = f.readAll(); 0335 // Random heuristics: strip one leading line which starts with "From ", also known as "the mailbox header". 0336 // Yeah, RFC 4155 says that there's a special MIME type application/mbox just for that, but nope, it's actually not being used. 0337 // So one gets ".eml" messages which are in fact not message/rfc822 stuff. 0338 if (content.startsWith("From ")) { 0339 auto pos = content.indexOf("\n"); 0340 if (pos > 0 0341 // random heuristic: don't chop off "too much" 0342 && pos < 80 0343 // random heiristic: three == one for "\n", two for CR LF which separates the headers from the body... 0344 && pos + 3 < content.size()) { 0345 content = content.mid(pos + 1 /* for the LF */); 0346 } 0347 } 0348 0349 static_cast<Imap::Mailbox::Model *>(sourceModel())->appendIntoMailbox( 0350 mailboxName, content, QStringList() << Imap::Mailbox::FlagNames::seen, 0351 QFileInfo(url.path()).lastModified()); 0352 ok = true; 0353 }); 0354 0355 return ok; 0356 } 0357 0358 void MailboxModel::handleRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) 0359 { 0360 TreeItemMailbox *parentMbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer())); 0361 if (parent.internalPointer() && ! parentMbox) 0362 return; 0363 if (! parentMbox) 0364 parentMbox = static_cast<Imap::Mailbox::Model *>(sourceModel())->m_mailboxes; 0365 Q_ASSERT(first >= 1); 0366 Q_ASSERT(last <= parentMbox->m_children.size() - 1); 0367 Q_ASSERT(first <= last); 0368 beginRemoveRows(mapFromSource(parent), first - 1, last - 1); 0369 } 0370 0371 void MailboxModel::handleRowsRemoved(const QModelIndex &parent, int first, int last) 0372 { 0373 Q_UNUSED(first); 0374 Q_UNUSED(last); 0375 TreeItemMailbox *parentMbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer())); 0376 if (parent.internalPointer() && ! parentMbox) 0377 return; 0378 endRemoveRows(); 0379 } 0380 0381 void MailboxModel::handleRowsAboutToBeInserted(const QModelIndex &parent, int first, int last) 0382 { 0383 if (parent.internalPointer() && ! dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer()))) 0384 return; 0385 if (first == 0 && last == 0) 0386 return; 0387 beginInsertRows(mapFromSource(parent), first - 1, last - 1); 0388 } 0389 0390 void MailboxModel::handleRowsInserted(const QModelIndex &parent, int first, int last) 0391 { 0392 if (parent.internalPointer() && ! dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer()))) 0393 return; 0394 if (first == 0 && last == 0) 0395 return; 0396 endInsertRows(); 0397 } 0398 0399 0400 } 0401 }