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 }