Strip the mailbox header when uploading messages from files
[trojita.git] / src / Imap / Model / MailboxModel.cpp
blobcc3db78223970c79daa932d8e3fae0570b585f54
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 <QDebug>
24 #include <QFile>
25 #include <QFileInfo>
26 #include <QMimeData>
27 #include <QMimeDatabase>
28 #include "Imap/Model/DragAndDrop.h"
29 #include "Imap/Model/ItemRoles.h"
30 #include "Imap/Model/MailboxModel.h"
31 #include "Imap/Model/MailboxTree.h"
32 #include "Imap/Model/SpecialFlagNames.h"
34 namespace Imap
36 namespace Mailbox
39 /** @short Does this URL point to an Internet e-mail message, according to the MIME type? */
40 static bool isFileWithMimeMessage(const QUrl &url)
42 QMimeDatabase mimeDb; // the docs say this is cheap to construct
43 return url.isLocalFile() && mimeDb.mimeTypeForFile(url.path()).inherits(QStringLiteral("message/rfc822"));
46 MailboxModel::MailboxModel(QObject *parent, Model *model): QAbstractProxyModel(parent)
48 setSourceModel(model);
50 // FIXME: will need to be expanded when Model supports more signals...
51 connect(model, &QAbstractItemModel::modelAboutToBeReset, this, &MailboxModel::handleModelAboutToBeReset);
52 connect(model, &QAbstractItemModel::modelReset, this, &MailboxModel::handleModelReset);
53 connect(model, &QAbstractItemModel::layoutAboutToBeChanged, this, &QAbstractItemModel::layoutAboutToBeChanged);
54 connect(model, &QAbstractItemModel::layoutChanged, this, &QAbstractItemModel::layoutChanged);
55 connect(model, &QAbstractItemModel::dataChanged, this, &MailboxModel::handleDataChanged);
56 connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MailboxModel::handleRowsAboutToBeRemoved);
57 connect(model, &QAbstractItemModel::rowsRemoved, this, &MailboxModel::handleRowsRemoved);
58 connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this, &MailboxModel::handleRowsAboutToBeInserted);
59 connect(model, &QAbstractItemModel::rowsInserted, this, &MailboxModel::handleRowsInserted);
60 connect(model, &Model::messageCountPossiblyChanged, this, &MailboxModel::handleMessageCountPossiblyChanged);
63 QHash<int, QByteArray> MailboxModel::roleNames() const
65 static QHash<int, QByteArray> roleNames;
66 if (roleNames.isEmpty()) {
67 roleNames[RoleIsFetched] = "isFetched";
68 roleNames[RoleShortMailboxName] = "shortMailboxName";
69 roleNames[RoleMailboxName] = "mailboxName";
70 roleNames[RoleMailboxSeparator] = "mailboxSeparator";
71 roleNames[RoleMailboxHasChildMailboxes] = "mailboxHasChildMailboxes";
72 roleNames[RoleMailboxIsINBOX] = "mailboxIsINBOX";
73 roleNames[RoleMailboxIsSelectable] = "mailboxIsSelectable";
74 roleNames[RoleMailboxNumbersFetched] = "mailboxNumbersFetched";
75 roleNames[RoleTotalMessageCount] = "totalMessageCount";
76 roleNames[RoleUnreadMessageCount] = "unreadMessageCount";
77 roleNames[RoleRecentMessageCount] = "recentMessageCount";
78 roleNames[RoleMailboxItemsAreLoading] = "mailboxItemsAreLoading";
80 return roleNames;
83 void MailboxModel::handleModelAboutToBeReset()
85 beginResetModel();
88 void MailboxModel::handleModelReset()
90 endResetModel();
93 bool MailboxModel::hasChildren(const QModelIndex &parent) const
95 if (parent.isValid() && parent.column() != 0)
96 return false;
98 QModelIndex index = mapToSource(parent);
100 TreeItemMailbox *mbox = dynamic_cast<TreeItemMailbox *>(
101 static_cast<TreeItem *>(
102 index.internalPointer()
104 return mbox ?
105 mbox->hasChildMailboxes(static_cast<Model *>(sourceModel())) :
106 sourceModel()->hasChildren(index);
109 void MailboxModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
111 QModelIndex first = mapFromSource(topLeft);
112 QModelIndex second = mapFromSource(bottomRight);
114 if (! first.isValid() || ! second.isValid()) {
115 // It's something completely alien...
116 return;
119 if (first.parent() == second.parent() && first.column() == second.column()) {
120 emit dataChanged(first, second);
121 } else {
122 // FIXME: batched updates aren't used yet
123 Q_ASSERT(false);
124 return;
128 QModelIndex MailboxModel::index(int row, int column, const QModelIndex &parent) const
130 if (row < 0 || column != 0)
131 return QModelIndex();
133 if (parent.column() != 0 && parent.column() != -1)
134 return QModelIndex();
136 QModelIndex translatedParent = mapToSource(parent);
138 if (row < sourceModel()->rowCount(translatedParent) - 1) {
139 void *ptr = sourceModel()->index(row + 1, 0, translatedParent).internalPointer();
140 Q_ASSERT(ptr);
141 return createIndex(row, column, ptr);
142 } else {
143 return QModelIndex();
147 QModelIndex MailboxModel::parent(const QModelIndex &index) const
149 return mapFromSource(mapToSource(index).parent());
152 int MailboxModel::rowCount(const QModelIndex &parent) const
154 if (parent.column() != 0 && parent.column() != -1)
155 return 0;
156 int res = sourceModel()->rowCount(mapToSource(parent));
157 if (res > 0)
158 --res;
159 return res;
162 int MailboxModel::columnCount(const QModelIndex &parent) const
164 return parent.column() == 0 || parent.column() == -1 ? 1 : 0;
167 QModelIndex MailboxModel::mapToSource(const QModelIndex &proxyIndex) const
169 int row = proxyIndex.row();
170 if (row < 0 || proxyIndex.column() != 0)
171 return QModelIndex();
172 ++row;
173 return static_cast<Imap::Mailbox::Model *>(sourceModel())->createIndex(row, 0, proxyIndex.internalPointer());
176 QModelIndex MailboxModel::mapFromSource(const QModelIndex &sourceIndex) const
178 if (!sourceIndex.isValid())
179 return QModelIndex();
181 if (! dynamic_cast<Imap::Mailbox::TreeItemMailbox *>(
182 static_cast<Imap::Mailbox::TreeItem *>(sourceIndex.internalPointer())))
183 return QModelIndex();
185 int row = sourceIndex.row();
186 if (row == 0)
187 return QModelIndex();
188 if (row > 0)
189 --row;
190 if (sourceIndex.column() != 0)
191 return QModelIndex();
193 return createIndex(row, 0, sourceIndex.internalPointer());
196 QVariant MailboxModel::data(const QModelIndex &proxyIndex, int role) const
198 if (! proxyIndex.isValid() || proxyIndex.model() != this)
199 return QVariant();
201 if (proxyIndex.column() != 0)
202 return QVariant();
204 TreeItemMailbox *mbox = dynamic_cast<TreeItemMailbox *>(
205 static_cast<TreeItem *>(proxyIndex.internalPointer())
207 Q_ASSERT(mbox);
208 if (role > RoleBase && role < RoleInvalidLastOne)
209 return mbox->data(static_cast<Imap::Mailbox::Model *>(sourceModel()), role);
210 else
211 return QAbstractProxyModel::data(createIndex(proxyIndex.row(), 0, proxyIndex.internalPointer()), role);
214 void MailboxModel::handleMessageCountPossiblyChanged(const QModelIndex &mailbox)
216 QModelIndex translated = mapFromSource(mailbox);
217 if (translated.isValid()) {
218 emit dataChanged(translated, translated);
222 Qt::ItemFlags MailboxModel::flags(const QModelIndex &index) const
224 if (! index.isValid())
225 return QAbstractProxyModel::flags(index);
227 TreeItemMailbox *mbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(index.internalPointer()));
228 Q_ASSERT(mbox);
230 Qt::ItemFlags res = QAbstractProxyModel::flags(index);
231 if (!mbox->isSelectable()) {
232 res &= ~Qt::ItemIsSelectable;
233 res |= Qt::ItemIsEnabled;
235 if (static_cast<Model *>(sourceModel())->isNetworkAvailable()) {
236 res |= Qt::ItemIsDropEnabled;
238 return res;
241 Qt::DropActions MailboxModel::supportedDropActions() const
243 return Qt::CopyAction | Qt::MoveAction;
246 QStringList MailboxModel::mimeTypes() const
248 return QStringList() << MimeTypes::xTrojitaMessageList;
251 bool MailboxModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const
253 // At first, check for dropping of URLs. We have to handle this with a priority because otherwise mimeTypes() gets called,
254 // and we deliberately do not list our messages as URLs because our URLs are proprietary.
255 const auto urls = data->urls();
256 if (std::any_of(urls.begin(), urls.end(), isFileWithMimeMessage)) {
257 return true;
260 // We cannot delegate this to QAbstractProxyModel::canDropMimeData because that code delegates the decision
261 // to the *source* model. That's bad, because our source model doesn't know anything about drag-and-drops
262 // or MIME types.
263 // However, calling the default implementation *at this level* of proxy chain makes sure that this proxy's
264 // mimeTypes() and supportedDropActions() gets consulted, which is the correct thing to do.
265 return QAbstractItemModel::canDropMimeData(data, action, row, column, parent);
268 bool MailboxModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
269 int row, int column, const QModelIndex &parent)
271 Q_UNUSED(row); Q_UNUSED(column);
272 if (action != Qt::CopyAction && action != Qt::MoveAction)
273 return false;
275 if (! parent.isValid())
276 return false;
278 if (! static_cast<Model *>(sourceModel())->isNetworkAvailable())
279 return false;
281 TreeItemMailbox *target = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer()));
282 Q_ASSERT(target);
284 if (! target->isSelectable())
285 return false;
287 if (data->hasFormat(MimeTypes::xTrojitaMessageList)) {
288 return dropTrojitaMessageList(target->mailbox(), action, data->data(MimeTypes::xTrojitaMessageList));
289 } else if (data->hasUrls()) {
290 return dropFileUrlList(target->mailbox(), data->urls());
291 } else {
292 return false;
296 bool MailboxModel::dropTrojitaMessageList(const QString &mailboxName, const Qt::DropAction action, const QByteArray &encodedData)
298 QDataStream stream(&const_cast<QByteArray &>(encodedData), QIODevice::ReadOnly);
300 Q_ASSERT(!stream.atEnd());
301 QString origMboxName;
302 stream >> origMboxName;
303 TreeItemMailbox *origMbox = static_cast<Model *>(sourceModel())->findMailboxByName(origMboxName);
304 if (! origMbox) {
305 qDebug() << "Can't find original mailbox when performing a drag&drop on messages";
306 return false;
309 uint uidValidity;
310 stream >> uidValidity;
311 if (uidValidity != origMbox->syncState.uidValidity()) {
312 qDebug() << "UID validity for original mailbox got changed, can't copy messages";
313 return false;
316 Imap::Uids uids;
317 stream >> uids;
319 static_cast<Model *>(sourceModel())->copyMoveMessages(origMbox, mailboxName, uids,
320 (action == Qt::MoveAction) ? MOVE : COPY);
321 return true;
324 bool MailboxModel::dropFileUrlList(const QString &mailboxName, QList<QUrl> files)
326 bool ok = false;
328 files.erase(std::remove_if(files.begin(), files.end(), std::not1(std::ptr_fun(isFileWithMimeMessage))), files.end());
329 std::for_each(files.begin(), files.end(), [this, mailboxName, &ok](const QUrl &url){
330 QFile f(url.path());
331 if (!f.open(QIODevice::ReadOnly))
332 return;
334 auto content = f.readAll();
335 // Random heuristics: strip one leading line which starts with "From ", also known as "the mailbox header".
336 // Yeah, RFC 4155 says that there's a special MIME type application/mbox just for that, but nope, it's actually not being used.
337 // So one gets ".eml" messages which are in fact not message/rfc822 stuff.
338 if (content.startsWith("From ")) {
339 auto pos = content.indexOf("\n");
340 if (pos > 0
341 // random heuristic: don't chop off "too much"
342 && pos < 80
343 // random heiristic: three == one for "\n", two for CR LF which separates the headers from the body...
344 && pos + 3 < content.size()) {
345 content = content.mid(pos + 1 /* for the LF */);
349 static_cast<Imap::Mailbox::Model *>(sourceModel())->appendIntoMailbox(
350 mailboxName, content, QStringList() << Imap::Mailbox::FlagNames::seen,
351 QFileInfo(url.path()).lastModified());
352 ok = true;
355 return ok;
358 void MailboxModel::handleRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
360 TreeItemMailbox *parentMbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer()));
361 if (parent.internalPointer() && ! parentMbox)
362 return;
363 if (! parentMbox)
364 parentMbox = static_cast<Imap::Mailbox::Model *>(sourceModel())->m_mailboxes;
365 Q_ASSERT(first >= 1);
366 Q_ASSERT(last <= parentMbox->m_children.size() - 1);
367 Q_ASSERT(first <= last);
368 beginRemoveRows(mapFromSource(parent), first - 1, last - 1);
371 void MailboxModel::handleRowsRemoved(const QModelIndex &parent, int first, int last)
373 Q_UNUSED(first);
374 Q_UNUSED(last);
375 TreeItemMailbox *parentMbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer()));
376 if (parent.internalPointer() && ! parentMbox)
377 return;
378 endRemoveRows();
381 void MailboxModel::handleRowsAboutToBeInserted(const QModelIndex &parent, int first, int last)
383 if (parent.internalPointer() && ! dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer())))
384 return;
385 if (first == 0 && last == 0)
386 return;
387 beginInsertRows(mapFromSource(parent), first - 1, last - 1);
390 void MailboxModel::handleRowsInserted(const QModelIndex &parent, int first, int last)
392 if (parent.internalPointer() && ! dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(parent.internalPointer())))
393 return;
394 if (first == 0 && last == 0)
395 return;
396 endInsertRows();