diff --git a/src/Gui/AttachmentView.cpp b/src/Gui/AttachmentView.cpp index f0bbfd33..99cc1890 100644 --- a/src/Gui/AttachmentView.cpp +++ b/src/Gui/AttachmentView.cpp @@ -1,406 +1,413 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "AttachmentView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Common/Paths.h" #include "Gui/MessageView.h" // so that the compiler knows it's a QObject #include "Gui/Window.h" #include "Imap/Network/FileDownloadManager.h" #include "Imap/Model/DragAndDrop.h" #include "Imap/Model/MailboxTree.h" #include "Imap/Model/ItemRoles.h" #include "UiUtils/Formatting.h" #include "UiUtils/IconLoader.h" namespace Gui { AttachmentView::AttachmentView(QWidget *parent, Imap::Network::MsgPartNetAccessManager *manager, const QModelIndex &partIndex, MessageView *messageView, QWidget *contentWidget) : QFrame(parent) , m_partIndex(partIndex) , m_messageView(messageView) , m_downloadAttachment(nullptr) , m_openAttachment(nullptr) , m_showHideAttachment(nullptr) , m_showSource(nullptr) , m_netAccess(manager) , m_tmpFile(nullptr) , m_contentWidget(contentWidget) { setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); setFrameStyle(QFrame::NoFrame); setCursor(Qt::OpenHandCursor); setAttribute(Qt::WA_Hover); // not actually required, but styles may assume the parameter and segfault on nullptr deref QStyleOption opt; opt.initFrom(this); const int padding = style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, this); setContentsMargins(padding, 0, padding, 0); QHBoxLayout *layout = new QHBoxLayout(); layout->setContentsMargins(0,0,0,0); // should be PM_LayoutHorizontalSpacing, but is not implemented by many Qt4 styles -including oxygen- for other conflicts int spacing = style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing, &opt, this); if (spacing < 0) spacing = style()->pixelMetric(QStyle::PM_DefaultLayoutSpacing, &opt, this); layout->setSpacing(0); m_menu = new QMenu(this); m_downloadAttachment = m_menu->addAction(UiUtils::loadIcon(QStringLiteral("document-save-as")), tr("Download")); m_openAttachment = m_menu->addAction(tr("Open Directly")); connect(m_downloadAttachment, &QAction::triggered, this, &AttachmentView::slotDownloadAttachment); connect(m_openAttachment, &QAction::triggered, this, &AttachmentView::slotOpenAttachment); connect(m_menu, &QMenu::aboutToShow, this, &AttachmentView::updateShowHideAttachmentState); if (m_contentWidget) { m_showHideAttachment = m_menu->addAction(UiUtils::loadIcon(QStringLiteral("view-preview")), tr("Show Preview")); m_showHideAttachment->setCheckable(true); m_showHideAttachment->setChecked(!m_contentWidget->isHidden()); connect(m_showHideAttachment, &QAction::triggered, m_contentWidget, &QWidget::setVisible); connect(m_showHideAttachment, &QAction::triggered, this, &AttachmentView::updateShowHideAttachmentState); } if (partIndex.data(Imap::Mailbox::RolePartMimeType).toByteArray() == "message/rfc822") { m_showSource = m_menu->addAction(UiUtils::loadIcon(QStringLiteral("text-x-hex")), tr("Show Message Source")); connect(m_showSource, &QAction::triggered, this, &AttachmentView::showMessageSource); } // Icon on the left m_icon = new QToolButton(this); m_icon->setAttribute(Qt::WA_NoMousePropagation, false); // inform us for DnD m_icon->setAutoRaise(true); m_icon->setIconSize(QSize(22,22)); m_icon->setToolButtonStyle(Qt::ToolButtonIconOnly); m_icon->setPopupMode(QToolButton::MenuButtonPopup); m_icon->setMenu(m_menu); connect(m_icon, &QAbstractButton::pressed, this, &AttachmentView::toggleIconCursor); connect(m_icon, &QAbstractButton::clicked, this, &AttachmentView::showMenuOrPreview); connect(m_icon, &QAbstractButton::released, this, &AttachmentView::toggleIconCursor); m_icon->setCursor(Qt::ArrowCursor); QString mimeDescription = partIndex.data(Imap::Mailbox::RolePartMimeType).toString(); QString rawMime = mimeDescription; QMimeType mimeType = QMimeDatabase().mimeTypeForName(mimeDescription); - if (mimeType.isValid() && !mimeType.isDefault()) { + if (rawMime == QStringLiteral("application/x-trojita-malformed-part-from-imap-response")) { + mimeDescription = QString::fromUtf8(partIndex.data(Imap::Mailbox::RolePartBodyFldParam) + .value() + .value("x-trojita-original-mime-type")); + mimeDescription = tr("IMAP Server error for this part: %1 (%2)").arg( + QMimeDatabase().mimeTypeForName(mimeDescription).comment(), mimeDescription); + m_icon->setIcon(UiUtils::loadIcon(QStringLiteral("emblem-warning"))); + } else if (mimeType.isValid() && !mimeType.isDefault()) { mimeDescription = mimeType.comment(); QIcon icon; if (rawMime == QLatin1String("message/rfc822")) { // Special case for plain e-mail messages. Motivation for this is that most of the OSes ship these icons // with a pixmap which shows something like a sheet of paper as the background. I find it rather dumb // to do this in the context of a MUA where attached messages are pretty common, which is why this special // case is in place. Comments welcome. icon = UiUtils::loadIcon(QStringLiteral("trojita")); } else { icon = QIcon::fromTheme(mimeType.iconName(), QIcon::fromTheme(mimeType.genericIconName(), UiUtils::loadIcon(QStringLiteral("mail-attachment"))) ); } m_icon->setIcon(icon); } else { m_icon->setIcon(UiUtils::loadIcon(QStringLiteral("mail-attachment"))); } layout->addWidget(m_icon); // space between icon and label layout->addSpacing(spacing); QVBoxLayout *subLayout = new QVBoxLayout; subLayout->setContentsMargins(0,0,0,0); // The file name shall be mouse-selectable m_fileName = new QLabel(this); m_fileName->setTextFormat(Qt::PlainText); m_fileName->setText(partIndex.data(Imap::Mailbox::RolePartFileName).toString()); m_fileName->setTextInteractionFlags(Qt::TextSelectableByMouse); m_fileName->setCursor(Qt::IBeamCursor); m_fileName->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); subLayout->addWidget(m_fileName); // Some metainformation -- the MIME type and the file size QLabel *lbl = new QLabel(tr("%2, %3").arg(mimeDescription, UiUtils::Formatting::prettySize(partIndex.data(Imap::Mailbox::RolePartOctets).toULongLong()))); if (rawMime != mimeDescription) { lbl->setToolTip(rawMime); } QFont f(lbl->font()); f.setItalic(true); if (f.pointSize() > -1) // don't try the below on pixel fonts ever. f.setPointSizeF(f.pointSizeF() * 0.8); lbl->setFont(f); subLayout->addWidget(lbl); layout->addLayout(subLayout); // space between label and arrow layout->addSpacing(spacing); layout->addStretch(100); QVBoxLayout *contentLayout = new QVBoxLayout(this); contentLayout->addLayout(layout); if (m_contentWidget) { contentLayout->addWidget(m_contentWidget); m_contentWidget->setCursor(Qt::ArrowCursor); } updateShowHideAttachmentState(); } void AttachmentView::slotDownloadAttachment() { m_downloadAttachment->setEnabled(false); Imap::Network::FileDownloadManager *manager = new Imap::Network::FileDownloadManager(this, m_netAccess, m_partIndex); connect(manager, &Imap::Network::FileDownloadManager::fileNameRequested, this, &AttachmentView::slotFileNameRequested); connect(manager, &Imap::Network::FileDownloadManager::transferError, m_messageView, &MessageView::transferError); connect(manager, &Imap::Network::FileDownloadManager::transferError, this, &AttachmentView::enableDownloadAgain); connect(manager, &Imap::Network::FileDownloadManager::transferError, manager, &QObject::deleteLater); connect(manager, &Imap::Network::FileDownloadManager::cancelled, this, &AttachmentView::enableDownloadAgain); connect(manager, &Imap::Network::FileDownloadManager::cancelled, manager, &QObject::deleteLater); connect(manager, &Imap::Network::FileDownloadManager::succeeded, this, &AttachmentView::enableDownloadAgain); connect(manager, &Imap::Network::FileDownloadManager::succeeded, manager, &QObject::deleteLater); manager->downloadPart(); } void AttachmentView::slotOpenAttachment() { m_openAttachment->setEnabled(false); Imap::Network::FileDownloadManager *manager = new Imap::Network::FileDownloadManager(this, m_netAccess, m_partIndex); connect(manager, &Imap::Network::FileDownloadManager::fileNameRequested, this, &AttachmentView::slotFileNameRequestedOnOpen); connect(manager, &Imap::Network::FileDownloadManager::transferError, m_messageView, &MessageView::transferError); connect(manager, &Imap::Network::FileDownloadManager::transferError, this, &AttachmentView::onOpenFailed); connect(manager, &Imap::Network::FileDownloadManager::transferError, manager, &QObject::deleteLater); // we aren't connecting to cancelled() as it cannot really happen -- the filename is never empty connect(manager, &Imap::Network::FileDownloadManager::succeeded, this, &AttachmentView::openDownloadedAttachment); connect(manager, &Imap::Network::FileDownloadManager::succeeded, manager, &QObject::deleteLater); manager->downloadPart(); } void AttachmentView::slotFileNameRequestedOnOpen(QString *fileName) { Q_ASSERT(!m_tmpFile); m_tmpFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/trojita-attachment-XXXXXX-") + fileName->replace(QLatin1Char('/'), QLatin1Char('_'))); m_tmpFile->setAutoRemove(false); m_tmpFile->open(); *fileName = m_tmpFile->fileName(); } void AttachmentView::slotFileNameRequested(QString *fileName) { static QDir lastDir = QDir(Common::writablePath(Common::LOCATION_DOWNLOAD)); if (!lastDir.exists()) lastDir = QDir(Common::writablePath(Common::LOCATION_DOWNLOAD)); QString fileLocation = lastDir.filePath(*fileName); *fileName = QFileDialog::getSaveFileName(this, tr("Save Attachment"), fileLocation, QString(), nullptr, QFileDialog::HideNameFilterDetails); if (!fileName->isEmpty()) lastDir = QFileInfo(*fileName).absoluteDir(); } void AttachmentView::enableDownloadAgain() { m_downloadAttachment->setEnabled(true); } void AttachmentView::onOpenFailed() { delete m_tmpFile; m_tmpFile = nullptr; m_openAttachment->setEnabled(true); } void AttachmentView::openDownloadedAttachment() { Q_ASSERT(m_tmpFile); // Make sure that the file is read-only so that the launched application does not attempt to modify it m_tmpFile->setPermissions(QFile::ReadOwner); QDesktopServices::openUrl(QUrl::fromLocalFile(m_tmpFile->fileName())); delete m_tmpFile; m_tmpFile = nullptr; m_openAttachment->setEnabled(true); } bool AttachmentView::previewIsShown() const { return m_contentWidget && m_contentWidget->isVisibleTo(const_cast(this)); } void AttachmentView::updateShowHideAttachmentState() { if (m_showHideAttachment) { m_showHideAttachment->setChecked(previewIsShown()); } } void AttachmentView::showMenuOrPreview() { if (previewIsShown() || !m_contentWidget) { showMenu(); } else { m_showHideAttachment->trigger(); } } void AttachmentView::showMenu() { if (QToolButton *btn = qobject_cast(sender())) { btn->setDown(false); } QPoint p = QCursor::pos(); p.rx() -= m_menu->width()/2; m_menu->popup(p); } void AttachmentView::toggleIconCursor() { if (m_icon->isDown()) m_icon->setCursor(Qt::OpenHandCursor); else m_icon->setCursor(Qt::ArrowCursor); } void AttachmentView::indicateHover() { if (m_menu->isVisible() || rect().contains(mapFromGlobal(QCursor::pos()))) { // WA_UnderMouse is wrong if (!autoFillBackground()) { setAutoFillBackground(true); QPalette pal(palette()); QLinearGradient grad(0,0,0,height()); grad.setColorAt(0, pal.color(backgroundRole())); grad.setColorAt(0.15, pal.color(backgroundRole()).lighter(110)); grad.setColorAt(0.8, pal.color(backgroundRole()).darker(110)); grad.setColorAt(1, pal.color(backgroundRole())); pal.setBrush(backgroundRole(), grad); setPalette(pal); } } else { setAutoFillBackground(false); setPalette(QPalette()); } } void AttachmentView::mousePressEvent(QMouseEvent *event) { event->accept(); if (event->button() == Qt::RightButton) { showMenu(); return; } m_dragStartPos = event->pos(); QFrame::mousePressEvent(event); } void AttachmentView::mouseMoveEvent(QMouseEvent *event) { QFrame::mouseMoveEvent(event); if (!(event->buttons() & Qt::LeftButton)) { return; } if ((m_dragStartPos - event->pos()).manhattanLength() < QApplication::startDragDistance()) return; QMimeData *mimeData = Imap::Mailbox::mimeDataForDragAndDrop(m_partIndex); if (!mimeData) return; event->accept(); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); drag->setHotSpot(event->pos()); drag->exec(Qt::CopyAction, Qt::CopyAction); } void AttachmentView::paintEvent(QPaintEvent *event) { QFrame::paintEvent(event); QPainter p(this); const int x = m_icon->geometry().width() + m_fileName->sizeHint().width() + 32; if (x >= rect().width()) return; QLinearGradient grad(x, 0, rect().right(), 0); const QColor c = testAttribute(Qt::WA_UnderMouse) ? palette().color(QPalette::Highlight) : palette().color(backgroundRole()).darker(120); grad.setColorAt(0, palette().color(backgroundRole())); grad.setColorAt(0.5, c); grad.setColorAt(1, palette().color(backgroundRole())); p.setBrush(grad); p.setPen(Qt::NoPen); p.drawRect(x, m_fileName->geometry().center().y(), width(), 1); p.end(); } QString AttachmentView::quoteMe() const { const AbstractPartWidget *widget = dynamic_cast(m_contentWidget); return widget && !m_contentWidget->isHidden() ? widget->quoteMe() : QString(); } bool AttachmentView::searchDialogRequested() { if (AbstractPartWidget *widget = dynamic_cast(m_contentWidget)) return widget->searchDialogRequested(); return false; } #define IMPL_PART_FORWARD_ONE_METHOD(METHOD) \ void AttachmentView::METHOD() \ {\ if (AbstractPartWidget *w = dynamic_cast(m_contentWidget)) \ w->METHOD(); \ } IMPL_PART_FORWARD_ONE_METHOD(reloadContents) IMPL_PART_FORWARD_ONE_METHOD(zoomIn) IMPL_PART_FORWARD_ONE_METHOD(zoomOut) IMPL_PART_FORWARD_ONE_METHOD(zoomOriginal) void AttachmentView::showMessageSource() { auto w = MainWindow::messageSourceWidget(m_partIndex); w->setWindowTitle(tr("Source of Attached Message")); w->show(); } } diff --git a/src/Imap/Parser/Message.cpp b/src/Imap/Parser/Message.cpp index ccbc3c0e..361ece24 100644 --- a/src/Imap/Parser/Message.cpp +++ b/src/Imap/Parser/Message.cpp @@ -1,872 +1,888 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include "Message.h" #include "MailAddress.h" #include "LowLevelParser.h" #include "../Model/MailboxTree.h" #include "../Encoders.h" #include "../Parser/Rfc5322HeaderParser.h" namespace Imap { namespace Message { QList Envelope::getListOfAddresses(const QVariant &in, const QByteArray &line, const int start) { if (in.type() == QVariant::ByteArray) { if (! in.toByteArray().isNull()) throw UnexpectedHere("getListOfAddresses: byte array not null", line, start); } else if (in.type() != QVariant::List) { throw ParseError("getListOfAddresses: not a list", line, start); } QVariantList list = in.toList(); QList res; for (QVariantList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { if (it->type() != QVariant::List) throw UnexpectedHere("getListOfAddresses: split item not a list", line, start); // FIXME: wrong offset res.append(MailAddress(it->toList(), line, start)); } return res; } Envelope Envelope::fromList(const QVariantList &items, const QByteArray &line, const int start) { if (items.size() != 10) throw ParseError("Envelope::fromList: size != 10", line, start); // FIXME: wrong offset // date QDateTime date; if (items[0].type() == QVariant::ByteArray) { QByteArray dateStr = items[0].toByteArray(); if (! dateStr.isEmpty()) { try { date = LowLevelParser::parseRFC2822DateTime(dateStr); } catch (ParseError &) { // FIXME: log this //throw ParseError( e.what(), line, start ); } } } // Otherwise it's "invalid", null. QString subject = Imap::decodeRFC2047String(items[1].toByteArray()); QList from, sender, replyTo, to, cc, bcc; from = Envelope::getListOfAddresses(items[2], line, start); sender = Envelope::getListOfAddresses(items[3], line, start); replyTo = Envelope::getListOfAddresses(items[4], line, start); to = Envelope::getListOfAddresses(items[5], line, start); cc = Envelope::getListOfAddresses(items[6], line, start); bcc = Envelope::getListOfAddresses(items[7], line, start); LowLevelParser::Rfc5322HeaderParser headerParser; if (items[8].type() != QVariant::ByteArray) throw UnexpectedHere("Envelope::fromList: inReplyTo not a QByteArray", line, start); QByteArray inReplyTo = items[8].toByteArray(); if (items[9].type() != QVariant::ByteArray) throw UnexpectedHere("Envelope::fromList: messageId not a QByteArray", line, start); QByteArray messageId = items[9].toByteArray(); QByteArray buf; if (!messageId.isEmpty()) buf += "Message-Id: " + messageId + "\r\n"; if (!inReplyTo.isEmpty()) buf += "In-Reply-To: " + inReplyTo + "\r\n"; if (!buf.isEmpty()) { bool ok = headerParser.parse(buf); if (!ok) { qDebug() << "Envelope::fromList: malformed headers"; } } // If the Message-Id fails to parse, well, bad luck. This enforced sanitizaion is hopefully better than // generating garbage in outgoing e-mails. messageId = headerParser.messageId.size() == 1 ? headerParser.messageId.front() : QByteArray(); return Envelope(date, subject, from, sender, replyTo, to, cc, bcc, headerParser.inReplyTo, messageId); } void Envelope::clear() { date = QDateTime(); subject.clear(); from.clear(); sender.clear(); replyTo.clear(); to.clear(); cc.clear(); bcc.clear(); inReplyTo.clear(); messageId.clear(); } bool OneMessage::eq(const AbstractData &other) const { try { const OneMessage &o = dynamic_cast(other); return o.mediaType == mediaType && mediaSubType == o.mediaSubType && bodyFldParam == o.bodyFldParam && bodyFldId == o.bodyFldId && bodyFldDesc == o.bodyFldDesc && bodyFldEnc == o.bodyFldEnc && bodyFldOctets == o.bodyFldOctets && bodyFldMd5 == o.bodyFldMd5 && bodyFldDsp == o.bodyFldDsp && bodyFldLang == o.bodyFldLang && bodyFldLoc == o.bodyFldLoc && bodyExtension == o.bodyExtension; } catch (std::bad_cast &) { return false; } } bool TextMessage::eq(const AbstractData &other) const { try { const TextMessage &o = dynamic_cast(other); return OneMessage::eq(o) && bodyFldLines == o.bodyFldLines; } catch (std::bad_cast &) { return false; } } QTextStream &TextMessage::dump(QTextStream &s, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); return s << QByteArray(indent, ' ') << "TextMessage( " << mediaType << "/" << mediaSubType << lf << i << "body-fld-param: " << bodyFldParam << lf << i << "body-fld-id: " << bodyFldId << lf << i << "body-fld-desc: " << bodyFldDesc << lf << i << "body-fld-enc: " << bodyFldEnc << lf << i << "body-fld-octets: " << bodyFldOctets << lf << i << "bodyFldMd5: " << bodyFldMd5 << lf << i << "body-fld-dsp: " << bodyFldDsp << lf << i << "body-fld-lang: " << bodyFldLang << lf << i << "body-fld-loc: " << bodyFldLoc << lf << i << "body-extension is " << bodyExtension.typeName() << lf << i << "body-fld-lines: " << bodyFldLines << lf << QByteArray(indent, ' ') << ")"; // FIXME: operator<< for QVariant... } bool MsgMessage::eq(const AbstractData &other) const { try { const MsgMessage &o = dynamic_cast(other); if (o.body) { if (body) { if (*body != *o.body) { return false; } } else { return false; } } else if (body) { return false; } return OneMessage::eq(o) && bodyFldLines == o.bodyFldLines && envelope == o.envelope; } catch (std::bad_cast &) { return false; } } QTextStream &MsgMessage::dump(QTextStream &s, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); s << QByteArray(indent, ' ') << "MsgMessage(" << lf; envelope.dump(s, indent + 1); s << i << "body-fld-lines " << bodyFldLines << lf << i << "body:" << lf; s << i << "body-fld-param: " << bodyFldParam << lf << i << "body-fld-id: " << bodyFldId << lf << i << "body-fld-desc: " << bodyFldDesc << lf << i << "body-fld-enc: " << bodyFldEnc << lf << i << "body-fld-octets: " << bodyFldOctets << lf << i << "bodyFldMd5: " << bodyFldMd5 << lf << i << "body-fld-dsp: " << bodyFldDsp << lf << i << "body-fld-lang: " << bodyFldLang << lf << i << "body-fld-loc: " << bodyFldLoc << lf << i << "body-extension is " << bodyExtension.typeName() << lf; if (body) body->dump(s, indent + 2); else s << i << " (null)"; return s << lf << QByteArray(indent, ' ') << ")"; } QTextStream &BasicMessage::dump(QTextStream &s, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); return s << QByteArray(indent, ' ') << "BasicMessage( " << mediaType << "/" << mediaSubType << lf << i << "body-fld-param: " << bodyFldParam << lf << i << "body-fld-id: " << bodyFldId << lf << i << "body-fld-desc: " << bodyFldDesc << lf << i << "body-fld-enc: " << bodyFldEnc << lf << i << "body-fld-octets: " << bodyFldOctets << lf << i << "bodyFldMd5: " << bodyFldMd5 << lf << i << "body-fld-dsp: " << bodyFldDsp << lf << i << "body-fld-lang: " << bodyFldLang << lf << i << "body-fld-loc: " << bodyFldLoc << lf << i << "body-extension is " << bodyExtension.typeName() << lf << QByteArray(indent, ' ') << ")"; // FIXME: operator<< for QVariant... } bool MultiMessage::eq(const AbstractData &other) const { try { const MultiMessage &o = dynamic_cast(other); if (bodies.count() != o.bodies.count()) { return false; } for (int i = 0; i < bodies.count(); ++i) { if (bodies[i]) { if (o.bodies[i]) { if (*bodies[i] != *o.bodies[i]) { return false; } } else { return false; } } else if (! o.bodies[i]) { return false; } } return mediaSubType == o.mediaSubType && bodyFldParam == o.bodyFldParam && bodyFldDsp == o.bodyFldDsp && bodyFldLang == o.bodyFldLang && bodyFldLoc == o.bodyFldLoc && bodyExtension == o.bodyExtension; } catch (std::bad_cast &) { return false; } } QTextStream &MultiMessage::dump(QTextStream &s, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); s << QByteArray(indent, ' ') << "MultiMessage( multipart/" << mediaSubType << lf << i << "body-fld-param " << bodyFldParam << lf << i << "body-fld-dsp " << bodyFldDsp << lf << i << "body-fld-lang " << bodyFldLang << lf << i << "body-fld-loc " << bodyFldLoc << lf << i << "bodyExtension is " << bodyExtension.typeName() << lf << i << "bodies: [ " << lf; for (QList >::const_iterator it = bodies.begin(); it != bodies.end(); ++it) if (*it) { (**it).dump(s, indent + 2); s << lf; } else s << i << " (null)" << lf; return s << QByteArray(indent, ' ') << "] )"; } AbstractMessage::bodyFldParam_t AbstractMessage::makeBodyFldParam(const QVariant &input, const QByteArray &line, const int start) { bodyFldParam_t map; if (input.type() != QVariant::List) { if (input.type() == QVariant::ByteArray && input.toByteArray().isNull()) return map; throw UnexpectedHere("body-fld-param: not a list / nil", line, start); } QVariantList list = input.toList(); if (list.size() % 2) throw UnexpectedHere("body-fld-param: wrong number of entries", line, start); for (int j = 0; j < list.size(); j += 2) if (list[j].type() != QVariant::ByteArray || list[j+1].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-param: string not found", line, start); else map[ list[j].toByteArray().toUpper() ] = list[j+1].toByteArray(); return map; } AbstractMessage::bodyFldDsp_t AbstractMessage::makeBodyFldDsp(const QVariant &input, const QByteArray &line, const int start) { bodyFldDsp_t res; if (input.type() != QVariant::List) { if (input.type() == QVariant::ByteArray) { if (input.toByteArray().isNull()) { return res; } else { qDebug() << "IMAP Parser warning: body-fld-dsp not a list or nil, got this instead: " << input.toByteArray(); return res; } } throw UnexpectedHere("body-fld-dsp: not a list / nil", line, start); } QVariantList list = input.toList(); if (list.size() < 1) { throw ParseError("body-fld-dsp: empty list is not allowed", line, start); } if (list[0].type() != QVariant::ByteArray) { throw UnexpectedHere("body-fld-dsp: first item is not a string", line, start); } res.first = list[0].toByteArray(); if (list.size() > 2) { throw ParseError("body-fld-dsp: too many items in the list", line, start); } else if (list.size() == 2) { res.second = makeBodyFldParam(list[1], line, start); } else { qDebug() << "IMAP Parser warning: body-fld-dsp: second item not present, ignoring"; } return res; } QList AbstractMessage::makeBodyFldLang(const QVariant &input, const QByteArray &line, const int start) { QList res; if (input.type() == QVariant::ByteArray) { if (input.toByteArray().isNull()) // handle NIL return res; res << input.toByteArray(); } else if (input.type() == QVariant::List) { QVariantList list = input.toList(); for (QVariantList::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) if (it->type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-lang has wrong structure", line, start); else res << it->toByteArray(); } else throw UnexpectedHere("body-fld-lang not found", line, start); return res; } uint AbstractMessage::extractUInt(const QVariant &var, const QByteArray &line, const int start) { if (var.type() == QVariant::UInt) return var.toUInt(); if (var.type() == QVariant::ByteArray) { bool ok = false; int number = var.toInt(&ok); if (ok) { if (number >= 0) { return number; } else { qDebug() << "Parser warning:" << number << "is not an unsigned int"; return 0; } } else if (var.toByteArray().isEmpty()) { qDebug() << "Parser warning: expected unsigned int, but got NIL or an empty string instead, yuck"; return 0; } else { throw UnexpectedHere("extractUInt: not a number", line, start); } } throw UnexpectedHere("extractUInt: weird data type", line, start); } quint64 AbstractMessage::extractUInt64(const QVariant &var, const QByteArray &line, const int start) { if (var.type() == QVariant::ULongLong) return var.toULongLong(); if (var.type() == QVariant::ByteArray) { bool ok = false; qint64 number = var.toLongLong(&ok); if (ok) { if (number >= 0) { return number; } else { qDebug() << "Parser warning:" << number << "is not an unsigned 64 bit int"; return 0; } } else if (var.toByteArray().isEmpty()) { qDebug() << "Parser warning: expected unsigned 64 bit int, but got NIL or an empty string instead, yuck"; return 0; } else { throw UnexpectedHere("extractUInt64: not a number", line, start); } } throw UnexpectedHere("extractUInt64: weird data type", line, start); } QSharedPointer AbstractMessage::fromList(const QVariantList &items, const QByteArray &line, const int start) { if (items.size() < 2) throw NoData("AbstractMessage::fromList: no data", line, start); if (items[0].type() == QVariant::ByteArray) { // it's a single-part message, hurray int i = 0; QByteArray mediaType = items[i].toByteArray().toLower(); ++i; QByteArray mediaSubType = items[i].toByteArray().toLower(); ++i; if (items.size() < 7) { qDebug() << "AbstractMessage::fromList(): body-type-basic(?): yuck, too few items, using what we've got"; } bodyFldParam_t bodyFldParam; if (i < items.size()) { bodyFldParam = makeBodyFldParam(items[i], line, start); ++i; } QByteArray bodyFldId; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-id not recognized as a ByteArray", line, start); bodyFldId = items[i].toByteArray(); ++i; } QByteArray bodyFldDesc; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-desc not recognized as a ByteArray", line, start); bodyFldDesc = items[i].toByteArray(); ++i; } QByteArray bodyFldEnc; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-enc not recognized as a ByteArray", line, start); bodyFldEnc = items[i].toByteArray(); ++i; } quint64 bodyFldOctets = 0; if (i < items.size()) { bodyFldOctets = extractUInt64(items[i], line, start); ++i; } uint bodyFldLines = 0; Envelope envelope; QSharedPointer body; + // This is used when the IMAP response fails to parse in a catastrophic manner. It's better than refusing + // to work with that mailbox altogether. In future, we might get a client-side MIME Parser for this :). + // In the meanwhile, it's critical to override the MIME type properly so that the upper layers don't assert + // on, e.g., missing headers (or invalid indexes in there), etc. +#define RETURN_ERROR_BINARY_PART \ + qDebug() << "will return a fake raw part instead of a damaged" << QByteArray(mediaType + '/' + mediaSubType).data() << "part"; \ + bodyFldParam["x-trojita-original-mime-type"] = mediaType + '/' + mediaSubType; \ + return QSharedPointer(new BasicMessage("application", "x-trojita-malformed-part-from-imap-response", \ + bodyFldParam, bodyFldId, bodyFldDesc, bodyFldEnc, bodyFldOctets, \ + QByteArray(), bodyFldDsp_t(), QList(), QByteArray(), QVariant())) + enum { MESSAGE, TEXT, BASIC} kind; if (mediaType == "message" && mediaSubType == "rfc822") { // extract envelope, body, body-fld-lines if (items.size() < 10) throw NoData("too few fields for a Message-message", line, start); kind = MESSAGE; if (items[i].type() == QVariant::ByteArray && items[i].toByteArray().isEmpty()) { - // ENVELOPE is NIL, this shouldn't really happen - qDebug() << "AbstractMessage::fromList(): message/rfc822: yuck, got NIL for envelope"; + // ENVELOPE is NIL -- a server bug, but there's a chance that perhaps the body might still be readable... + qDebug() << "message/rfc822: yuck, got NIL for envelope"; } else if (items[i].type() != QVariant::List) { - throw UnexpectedHere("message/rfc822: envelope not a list", line, start); + qDebug() << "message/rfc822: yuck, ENVELOPE is not a list"; + RETURN_ERROR_BINARY_PART; } else { envelope = Envelope::fromList(items[i].toList(), line, start); } ++i; - if (items[i].type() != QVariant::List) - throw UnexpectedHere("message/rfc822: body not recognized as a list", line, start); - body = AbstractMessage::fromList(items[i].toList(), line, start); + if (items[i].type() != QVariant::List) { + // we're screwed, let's fall back to a binary part rendering + qDebug() << "message/rfc822: yuck, got garbage for BODY"; + RETURN_ERROR_BINARY_PART; + } else { + body = AbstractMessage::fromList(items[i].toList(), line, start); + } ++i; try { bodyFldLines = extractUInt(items[i], line, start); } catch (const UnexpectedHere &) { - qDebug() << "AbstractMessage::fromList(): message/rfc822: yuck, invalid body-fld-lines"; + qDebug() << "message/rfc822: yuck, invalid body-fld-lines"; } ++i; } else if (mediaType == "text") { kind = TEXT; if (i < items.size()) { // extract body-fld-lines bodyFldLines = extractUInt(items[i], line, start); ++i; } } else { // don't extract anything as we're done here kind = BASIC; } // extract body-ext-1part // body-fld-md5 QByteArray bodyFldMd5; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-md5 not a ByteArray", line, start); bodyFldMd5 = items[i].toByteArray(); ++i; } // body-fld-dsp bodyFldDsp_t bodyFldDsp; if (i < items.size()) { bodyFldDsp = makeBodyFldDsp(items[i], line, start); ++i; } // body-fld-lang QList bodyFldLang; if (i < items.size()) { bodyFldLang = makeBodyFldLang(items[i], line, start); ++i; } // body-fld-loc QByteArray bodyFldLoc; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-loc not found", line, start); bodyFldLoc = items[i].toByteArray(); ++i; } // body-extension QVariant bodyExtension; if (i < items.size()) { if (i == items.size() - 1) { bodyExtension = items[i]; ++i; } else { QVariantList list; for (; i < items.size(); ++i) list << items[i]; bodyExtension = list; } } switch (kind) { case MESSAGE: return QSharedPointer( new MsgMessage(mediaType, mediaSubType, bodyFldParam, bodyFldId, bodyFldDesc, bodyFldEnc, bodyFldOctets, bodyFldMd5, bodyFldDsp, bodyFldLang, bodyFldLoc, bodyExtension, envelope, body, bodyFldLines) ); case TEXT: return QSharedPointer( new TextMessage(mediaType, mediaSubType, bodyFldParam, bodyFldId, bodyFldDesc, bodyFldEnc, bodyFldOctets, bodyFldMd5, bodyFldDsp, bodyFldLang, bodyFldLoc, bodyExtension, bodyFldLines) ); case BASIC: default: return QSharedPointer( new BasicMessage(mediaType, mediaSubType, bodyFldParam, bodyFldId, bodyFldDesc, bodyFldEnc, bodyFldOctets, bodyFldMd5, bodyFldDsp, bodyFldLang, bodyFldLoc, bodyExtension) ); } } else if (items[0].type() == QVariant::List) { if (items.size() < 2) throw ParseError("body-type-mpart: structure should be \"body* string\"", line, start); int i = 0; QList > bodies; while (items[i].type() == QVariant::List) { bodies << fromList(items[i].toList(), line, start); ++i; } if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-type-mpart: media-subtype not recognized", line, start); QByteArray mediaSubType = items[i].toByteArray().toLower(); ++i; // body-ext-mpart // body-fld-param bodyFldParam_t bodyFldParam; if (i < items.size()) { bodyFldParam = makeBodyFldParam(items[i], line, start); ++i; } // body-fld-dsp bodyFldDsp_t bodyFldDsp; if (i < items.size()) { bodyFldDsp = makeBodyFldDsp(items[i], line, start); ++i; } // body-fld-lang QList bodyFldLang; if (i < items.size()) { bodyFldLang = makeBodyFldLang(items[i], line, start); ++i; } // body-fld-loc QByteArray bodyFldLoc; if (i < items.size()) { if (items[i].type() != QVariant::ByteArray) throw UnexpectedHere("body-fld-loc not found", line, start); bodyFldLoc = items[i].toByteArray(); ++i; } // body-extension QVariant bodyExtension; if (i < items.size()) { if (i == items.size() - 1) { bodyExtension = items[i]; ++i; } else { QVariantList list; for (; i < items.size(); ++i) list << items[i]; bodyExtension = list; } } return QSharedPointer( new MultiMessage(bodies, mediaSubType, bodyFldParam, bodyFldDsp, bodyFldLang, bodyFldLoc, bodyExtension)); } else { throw UnexpectedHere("AbstractMessage::fromList: invalid data type of first item", line, start); } } void dumpListOfAddresses(QTextStream &stream, const QList &list, const int indent) { QByteArray lf("\n"); switch (list.size()) { case 0: stream << "[ ]" << lf; break; case 1: stream << "[ " << list.front() << " ]" << lf; break; default: { QByteArray i(indent + 1, ' '); stream << "[" << lf; for (QList::const_iterator it = list.begin(); it != list.end(); ++it) stream << i << *it << lf; stream << QByteArray(indent, ' ') << "]" << lf; } } } QTextStream &Envelope::dump(QTextStream &stream, const int indent) const { QByteArray i(indent + 1, ' '); QByteArray lf("\n"); stream << QByteArray(indent, ' ') << "Envelope(" << lf << i << "Date: " << date.toString() << lf << i << "Subject: " << subject << lf; stream << i << "From: "; dumpListOfAddresses(stream, from, indent + 1); stream << i << "Sender: "; dumpListOfAddresses(stream, sender, indent + 1); stream << i << "Reply-To: "; dumpListOfAddresses(stream, replyTo, indent + 1); stream << i << "To: "; dumpListOfAddresses(stream, to, indent + 1); stream << i << "Cc: "; dumpListOfAddresses(stream, cc, indent + 1); stream << i << "Bcc: "; dumpListOfAddresses(stream, bcc, indent + 1); stream << i << "In-Reply-To: " << inReplyTo << lf << i << "Message-Id: " << messageId << lf; return stream << QByteArray(indent, ' ') << ")" << lf; } QTextStream &operator<<(QTextStream &stream, const Envelope &e) { return e.dump(stream, 0); } QTextStream &operator<<(QTextStream &stream, const AbstractMessage::bodyFldParam_t &p) { stream << "bodyFldParam[ "; bool first = true; for (AbstractMessage::bodyFldParam_t::const_iterator it = p.begin(); it != p.end(); ++it, first = false) stream << (first ? "" : ", ") << it.key() << ": " << it.value(); return stream << "]"; } QTextStream &operator<<(QTextStream &stream, const AbstractMessage::bodyFldDsp_t &p) { return stream << "bodyFldDsp( " << p.first << ", " << p.second << ")"; } QTextStream &operator<<(QTextStream &stream, const QList &list) { stream << "( "; bool first = true; for (QList::const_iterator it = list.begin(); it != list.end(); ++it, first = false) stream << (first ? "" : ", ") << *it; return stream << " )"; } bool operator==(const Envelope &a, const Envelope &b) { return a.date == b.date && a.subject == b.subject && a.from == b.from && a.sender == b.sender && a.replyTo == b.replyTo && a.to == b.to && a.cc == b.cc && a.bcc == b.bcc && a.inReplyTo == b.inReplyTo && a.messageId == b.messageId; } /** @short Extract interesting part-specific metadata from the BODYSTRUCTURE into the actual part Examples are stuff like the charset, or the suggested filename. */ void AbstractMessage::storeInterestingFields(Mailbox::TreeItemPart *p) const { p->setBodyFldParam(bodyFldParam); // Charset bodyFldParam_t::const_iterator it = bodyFldParam.find("CHARSET"); if (it != bodyFldParam.end()) { p->setCharset(*it); } // Support for format=flowed, RFC3676 it = bodyFldParam.find("FORMAT"); if (it != bodyFldParam.end()) { p->setContentFormat(*it); it = bodyFldParam.find("DELSP"); if (it != bodyFldParam.end()) { p->setContentDelSp(*it); } } // Filename and content-disposition if (!bodyFldDsp.first.isNull()) { p->setBodyDisposition(bodyFldDsp.first); p->setFileName(Imap::extractRfc2231Param(bodyFldDsp.second, "FILENAME")); } // Try to look for the obsolete "name" right in the Content-Type header (as parsed by the IMAP server) as a fallback // As per Thomas' suggestion, an empty-but-specified filename is happily overwritten here by design. if (p->fileName().isEmpty()) { p->setFileName(Imap::extractRfc2231Param(bodyFldParam, "NAME")); } } void OneMessage::storeInterestingFields(Mailbox::TreeItemPart *p) const { AbstractMessage::storeInterestingFields(p); p->setTransferEncoding(bodyFldEnc.toLower()); p->setOctets(bodyFldOctets); p->setBodyFldId(bodyFldId); } void MultiMessage::storeInterestingFields(Mailbox::TreeItemPart *p) const { AbstractMessage::storeInterestingFields(p); // The multipart/related can specify the root part to show if (mediaSubType == "related") { bodyFldParam_t::const_iterator it = bodyFldParam.find("START"); if (it != bodyFldParam.end()) { p->setMultipartRelatedStartPart(*it); } } } Mailbox::TreeItemChildrenList TextMessage::createTreeItems(Mailbox::TreeItem *parent) const { Mailbox::TreeItemChildrenList list; Mailbox::TreeItemPart *p = new Mailbox::TreeItemPart(parent, mediaType + "/" + mediaSubType); storeInterestingFields(p); list << p; return list; } Mailbox::TreeItemChildrenList BasicMessage::createTreeItems(Mailbox::TreeItem *parent) const { Mailbox::TreeItemChildrenList list; Mailbox::TreeItemPart *p = new Mailbox::TreeItemPart(parent, mediaType + "/" + mediaSubType); storeInterestingFields(p); list << p; return list; } Mailbox::TreeItemChildrenList MsgMessage::createTreeItems(Mailbox::TreeItem *parent) const { Mailbox::TreeItemChildrenList list; Mailbox::TreeItemPart *part = new Mailbox::TreeItemPartMultipartMessage(parent, envelope); part->setChildren(body->createTreeItems(part)); // always returns an empty list -> no need to qDeleteAll() storeInterestingFields(part); list << part; return list; } Mailbox::TreeItemChildrenList MultiMessage::createTreeItems(Mailbox::TreeItem *parent) const { Mailbox::TreeItemChildrenList list, list2; Mailbox::TreeItemPart *part = new Mailbox::TreeItemPart(parent, "multipart/" + mediaSubType); for (QList >::const_iterator it = bodies.begin(); it != bodies.end(); ++it) { list2 << (*it)->createTreeItems(part); } part->setChildren(list2); // always returns an empty list -> no need to qDeleteAll() storeInterestingFields(part); list << part; return list; } } } QDebug operator<<(QDebug dbg, const Imap::Message::Envelope &envelope) { using namespace Imap::Message; return dbg << "Envelope( FROM" << MailAddress::prettyList(envelope.from, MailAddress::FORMAT_READABLE) << "TO" << MailAddress::prettyList(envelope.to, MailAddress::FORMAT_READABLE) << "CC" << MailAddress::prettyList(envelope.cc, MailAddress::FORMAT_READABLE) << "BCC" << MailAddress::prettyList(envelope.bcc, MailAddress::FORMAT_READABLE) << "SUBJECT" << envelope.subject << "DATE" << envelope.date << "MESSAGEID" << envelope.messageId; } QDataStream &operator>>(QDataStream &stream, Imap::Message::Envelope &e) { return stream >> e.bcc >> e.cc >> e.date >> e.from >> e.inReplyTo >> e.messageId >> e.replyTo >> e.sender >> e.subject >> e.to; } QDataStream &operator<<(QDataStream &stream, const Imap::Message::Envelope &e) { return stream << e.bcc << e.cc << e.date << e.from << e.inReplyTo << e.messageId << e.replyTo << e.sender << e.subject << e.to; } diff --git a/src/UiUtils/PartWalker_impl.h b/src/UiUtils/PartWalker_impl.h index 87841fd9..3f61562c 100644 --- a/src/UiUtils/PartWalker_impl.h +++ b/src/UiUtils/PartWalker_impl.h @@ -1,219 +1,220 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include "UiUtils/PartWalker.h" #include "Imap/Model/ItemRoles.h" namespace UiUtils { template PartWalker::PartWalker(Imap::Network::MsgPartNetAccessManager *manager, Context context, std::unique_ptr > visitor) : m_manager(manager), m_netWatcher(0), m_context(context) { m_visitor = std::move(visitor); } template Result PartWalker::walk(const QModelIndex &partIndex,int recursionDepth, const UiUtils::PartLoadingOptions loadingMode) { using namespace Imap::Mailbox; Q_ASSERT(partIndex.isValid()); if (recursionDepth > 1000) { return m_visitor->visitError(PartWalker::tr("This message contains too deep nesting of MIME message parts.\n" "To prevent stack exhaustion and your head from exploding, only\n" "the top-most thousand items or so are shown."), 0); } QString mimeType = partIndex.data(Imap::Mailbox::RolePartMimeType).toString().toLower(); bool isMessageRfc822 = mimeType == QLatin1String("message/rfc822"); bool isCompoundMimeType = mimeType.startsWith(QLatin1String("multipart/")) || isMessageRfc822; if (loadingMode & PART_IS_HIDDEN) { return m_visitor->visitLoadablePart(0, m_manager, partIndex, this, recursionDepth + 1, loadingMode | PART_IGNORE_CLICKTHROUGH); } // Check if we are dealing with encrypted data if (mimeType == QLatin1String("multipart/encrypted")) { return m_visitor->visitMultipartEncryptedView(0, this, partIndex, recursionDepth, loadingMode); } // Check whether we can render this MIME type at all QStringList allowedMimeTypes; allowedMimeTypes << QStringLiteral("text/html") << QStringLiteral("text/plain") << QStringLiteral("image/jpeg") << QStringLiteral("image/jpg") << QStringLiteral("image/pjpeg") << QStringLiteral("image/png") << QStringLiteral("image/gif"); bool recognizedMimeType = isCompoundMimeType || allowedMimeTypes.contains(mimeType); bool isDerivedMimeType = false; if (!recognizedMimeType) { // QMimeType's docs say that one shall use inherit() to check for "is this a recognized MIME type". // E.g. text/x-csrc inherits text/plain. QMimeType partType = QMimeDatabase().mimeTypeForName(mimeType); Q_FOREACH(const QString &candidate, allowedMimeTypes) { if (partType.isValid() && !partType.isDefault() && partType.inherits(candidate)) { // Looks like we shall be able to show this recognizedMimeType = true; // If it's a derived MIME type, it makes sense to not block inline display, yet still make it possible to hide it // using the regular attachment controls isDerivedMimeType = true; m_manager->registerMimeTypeTranslation(mimeType, candidate); break; } } } // Check whether it shall be wrapped inside an AttachmentView // From section 2.8 of RFC 2183: "Unrecognized disposition types should be treated as `attachment'." const QByteArray contentDisposition = partIndex.data(Imap::Mailbox::RolePartBodyDisposition).toByteArray().toLower(); const bool isInline = (contentDisposition.isEmpty() || contentDisposition == "inline") && !(loadingMode & PART_IGNORE_INLINE); const bool looksLikeAttachment = !partIndex.data(Imap::Mailbox::RolePartFileName).toString().isEmpty(); + const bool isMimeParsingError = mimeType == QStringLiteral("application/x-trojita-malformed-part-from-imap-response"); const bool wrapInAttachmentView = !(loadingMode & PART_IGNORE_DISPOSITION_ATTACHMENT) - && (looksLikeAttachment || !isInline || !recognizedMimeType || isDerivedMimeType || isMessageRfc822); + && (looksLikeAttachment || !isInline || !recognizedMimeType || isDerivedMimeType || isMessageRfc822 || isMimeParsingError); if (wrapInAttachmentView) { // The problem is that some nasty MUAs (hint hint Thunderbird) would // happily attach a .tar.gz and call it "inline" Result contentView = 0; if (recognizedMimeType) { PartLoadingOptions options = loadingMode | PART_IGNORE_DISPOSITION_ATTACHMENT; if (!isInline) { // The widget will be hidden by default, i.e. the "inline preview" will be deactivated. // If the user clicks that action in the AttachmentView, it makes sense to load the plugin without any further ado, // without requiring an extra clickthrough options |= PART_IS_HIDDEN; } else if (!isCompoundMimeType) { // This is to prevent a clickthrough when the data can be already shown partIndex.data(Imap::Mailbox::RolePartForceFetchFromCache); // This makes sure that clickthrough only affects big parts during "expensive network" mode if ( (m_netWatcher && m_netWatcher->desiredNetworkPolicy() != Imap::Mailbox::NETWORK_EXPENSIVE) || partIndex.data(Imap::Mailbox::RolePartOctets).toULongLong() <= ExpensiveFetchThreshold) { options |= PART_IGNORE_CLICKTHROUGH; } } else { // A compound type -> make sure we disable clickthrough options |= PART_IGNORE_CLICKTHROUGH; } if (m_netWatcher && m_netWatcher->effectiveNetworkPolicy() == Imap::Mailbox::NETWORK_OFFLINE) { // This is to prevent a clickthrough when offline options |= PART_IGNORE_CLICKTHROUGH; } contentView = m_visitor->visitLoadablePart(0, m_manager, partIndex, this, recursionDepth + 1, options); if (!isInline) { m_visitor->applySetHidden(contentView); } } // Previously, we would also hide an attachment if the current policy was "expensive network", the part was too big // and not fetched yet. Arguably, that's a bug -- an item which is marked online shall not be hidden. // Wrapping via a clickthrough is fine, though; the user is clearly informed that this item *should* be visible, // yet the bandwidth is not excessively trashed. return m_visitor->visitAttachmentPart(0, m_manager, partIndex, m_context, contentView); } // Now we know for sure that it's not supposed to be wrapped in an AttachmentView, cool. if (mimeType.startsWith(QLatin1String("multipart/"))) { // it's a compound part if (mimeType == QLatin1String("multipart/alternative")) { return m_visitor->visitMultipartAlternative(0, this, partIndex, recursionDepth, loadingMode); } else if (mimeType == QLatin1String("multipart/signed")) { return m_visitor->visitMultipartSignedView(0, this, partIndex, recursionDepth, loadingMode); } else if (mimeType == QLatin1String("multipart/related")) { // The purpose of this section is to find a text/html e-mail, along with its associated body parts, and hide // everything else than the HTML widget. // At this point, it might be interesting to somehow respect the user's preference about using text/plain // instead of text/html. However, things are a bit complicated; the widget used at this point really wants // to either show just a single part or alternatively all of them in a sequence. // Furthermore, if someone sends a text/plain and a text/html together inside a multipart/related, they're // just wrong. // Let's see if we know what the root part is QModelIndex mainPartIndex; QVariant mainPartCID = partIndex.data(RolePartMultipartRelatedMainCid); if (mainPartCID.isValid()) { mainPartIndex = Imap::Network::MsgPartNetAccessManager::cidToPart(partIndex, mainPartCID.toByteArray()); } if (!mainPartIndex.isValid()) { // The Content-Type-based start parameter was not terribly useful. Let's find the HTML part manually. QModelIndex candidate = partIndex.child(0, 0); while (candidate.isValid()) { if (candidate.data(RolePartMimeType).toString() == QLatin1String("text/html")) { mainPartIndex = candidate; break; } candidate = candidate.sibling(candidate.row() + 1, 0); } } if (mainPartIndex.isValid()) { if (mainPartIndex.data(RolePartMimeType).toString() == QLatin1String("text/html")) { return walk(mainPartIndex, recursionDepth+1, loadingMode); } else { // Sorry, but anything else than text/html is by definition suspicious here. Better than picking some random // choice, let's just show everything. return m_visitor->visitGenericMultipartView(0, this, partIndex, recursionDepth, loadingMode); } } else { // The RFC2387's wording is clear that in absence of an explicit START argument, the first part is the starting one. // On the other hand, I've seen real-world messages whose first part is some utter garbage (an image sent as // application/octet-stream, for example) and some *other* part is an HTML text. In that case (and if we somehow // failed to pick the HTML part by a heuristic), it's better to show everything. return m_visitor->visitGenericMultipartView(0, this, partIndex, recursionDepth, loadingMode); } } else { return m_visitor->visitGenericMultipartView(0, this, partIndex, recursionDepth, loadingMode); } } else if (mimeType == QLatin1String("message/rfc822")) { return m_visitor->visitMessage822View(0, this, partIndex, recursionDepth, loadingMode); } else { partIndex.data(Imap::Mailbox::RolePartForceFetchFromCache); if ((loadingMode & PART_IGNORE_CLICKTHROUGH) || (loadingMode & PART_IGNORE_LOAD_ON_SHOW) || partIndex.data(Imap::Mailbox::RoleIsFetched).toBool() || (m_netWatcher && m_netWatcher->desiredNetworkPolicy() != Imap::Mailbox::NETWORK_EXPENSIVE ) || partIndex.data(Imap::Mailbox::RolePartOctets).toULongLong() < ExpensiveFetchThreshold) { // Show it directly without any fancy wrapping return m_visitor->visitSimplePartView(0, m_manager, partIndex, m_context); } else { return m_visitor->visitLoadablePart(0, m_manager, partIndex, this, recursionDepth + 1, (m_netWatcher && m_netWatcher->effectiveNetworkPolicy() != Imap::Mailbox::NETWORK_OFFLINE) ? loadingMode : loadingMode | PART_IGNORE_CLICKTHROUGH); } } } template Context PartWalker::context() const { return m_context; } template void PartWalker::setNetworkWatcher(Imap::Mailbox::NetworkWatcher *netWatcher) { m_netWatcher = netWatcher; } } diff --git a/tests/Imap/test_Imap_Parser_parse.cpp b/tests/Imap/test_Imap_Parser_parse.cpp index 393b1333..48e74938 100644 --- a/tests/Imap/test_Imap_Parser_parse.cpp +++ b/tests/Imap/test_Imap_Parser_parse.cpp @@ -1,1225 +1,1275 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include "Imap/Parser/Message.h" #include "Streams/FakeSocket.h" #include "test_Imap_Parser_parse.h" Q_DECLARE_METATYPE(QSharedPointer) Q_DECLARE_METATYPE(Imap::Responses::State) Q_DECLARE_METATYPE(Imap::Sequence) void ImapParserParseTest::initTestCase() { array.reset( new QByteArray() ); Streams::Socket* sock(new Streams::FakeSocket(Imap::CONN_STATE_CONNECTED_PRETLS_PRECAPS)); parser = new Imap::Parser( this, sock, 666 ); } void ImapParserParseTest::cleanupTestCase() { delete parser; parser = 0; QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete); } /** @short Test tagged response parsing */ void ImapParserParseTest::testParseTagged() { QFETCH( QByteArray, line ); QFETCH( QSharedPointer, response ); Q_ASSERT( response ); QCOMPARE( *(parser->parseTagged( line )), *response ); } void ImapParserParseTest::testParseTagged_data() { using namespace Imap::Responses; QTest::addColumn("line"); QTest::addColumn >("response"); QSharedPointer voidData( new RespData() ); QSharedPointer emptyList( new RespData( QStringList() ) ); QTest::newRow("tagged-ok-simple") << QByteArray("y01 OK Everything works, man!\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Everything works, man!"), NONE, voidData ) ); QTest::newRow("tagged-no-simple") << QByteArray("12345 NO Nope, something is broken\r\n") << QSharedPointer( new State("12345", NO, QStringLiteral("Nope, something is broken"), NONE, voidData ) ); QTest::newRow("tagged-bad-simple") << QByteArray("ahoj BaD WTF?\r\n") << QSharedPointer( new State("ahoj", BAD, QStringLiteral("WTF?"), NONE, voidData ) ); QTest::newRow("tagged-ok-alert") << QByteArray("y01 oK [ALERT] Server on fire\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Server on fire"), ALERT, voidData ) ); QTest::newRow("tagged-no-alert") << QByteArray("1337 no [ALeRT] Server on fire\r\n") << QSharedPointer( new State("1337", NO, QStringLiteral("Server on fire"), ALERT, voidData ) ); QTest::newRow("tagged-ok-capability") << QByteArray("y01 OK [CAPaBILITY blurdybloop IMAP4rev1 WTF] Capabilities updated\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Capabilities updated"), CAPABILITIES, QSharedPointer( new RespData( QStringList() << QStringLiteral("blurdybloop") << QStringLiteral("IMAP4rev1") << QStringLiteral("WTF")) ) ) ); QTest::newRow("tagged-ok-parse") << QByteArray("y01 OK [PArSE] Parse error. What do you feed me with?\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Parse error. What do you feed me with?"), PARSE, voidData) ); QTest::newRow("tagged-ok-permanentflags-empty") << QByteArray("y01 OK [PERMANENTfLAGS] Behold, the flags!\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Behold, the flags!"), PERMANENTFLAGS, emptyList) ); QTest::newRow("tagged-ok-permanentflags-flags") << QByteArray("y01 OK [PErMANENTFLAGS (\\Foo \\Bar SmrT)] Behold, the flags!\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Behold, the flags!"), PERMANENTFLAGS, QSharedPointer( new RespData( QStringList() << QStringLiteral("\\Foo") << QStringLiteral("\\Bar") << QStringLiteral("SmrT") ))) ); QTest::newRow("tagged-ok-permanentflags-flags-not-enclosed") << QByteArray("y01 OK [PErMANENTFLAGS \\Foo \\Bar SmrT] Behold, the flags!\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Behold, the flags!"), PERMANENTFLAGS, QSharedPointer( new RespData( QStringList() << QStringLiteral("\\Foo") << QStringLiteral("\\Bar") << QStringLiteral("SmrT") ))) ); QTest::newRow("tagged-ok-badcharset-empty") << QByteArray("y01 OK [BadCharset] foo\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("foo"), BADCHARSET, emptyList) ); QTest::newRow("tagged-ok-badcharset-something") << QByteArray("y01 OK [BADCHARSET (utf8)] wrong charset\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("wrong charset"), BADCHARSET, QSharedPointer( new RespData( QStringList() << QStringLiteral("utf8") ))) ); QTest::newRow("tagged-ok-badcharset-not-enclosed") << QByteArray("y01 OK [badcharset a ba cc] Behold, the charset!\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Behold, the charset!"), BADCHARSET, QSharedPointer( new RespData( QStringList() << QStringLiteral("a") << QStringLiteral("ba") << QStringLiteral("cc") ))) ); QTest::newRow("tagged-ok-readonly") << QByteArray("333 OK [ReAD-ONLY] No writing for you\r\n") << QSharedPointer( new State("333", OK, QStringLiteral("No writing for you"), READ_ONLY, voidData)); QTest::newRow("tagged-ok-readwrite") << QByteArray("y01 OK [REaD-WRITE] Write!!!\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Write!!!"), READ_WRITE, voidData)); QTest::newRow("tagged-ok-trycreate") << QByteArray("y01 OK [TryCreate] ...would be better :)\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("...would be better :)"), TRYCREATE, voidData)); QTest::newRow("tagged-ok-uidnext") << QByteArray("y01 OK [uidNext 5] Next UID\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("Next UID"), UIDNEXT, QSharedPointer( new RespData( 5 ) ))); QTest::newRow("tagged-ok-uidvalidity") << QByteArray("y01 OK [UIDVALIDITY 17] UIDs valid\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("UIDs valid"), UIDVALIDITY, QSharedPointer( new RespData( 17 ) ))); QTest::newRow("tagged-ok-unseen") << QByteArray("y01 OK [unSeen 666] I need my glasses\r\n") << QSharedPointer( new State("y01", OK, QStringLiteral("I need my glasses"), UNSEEN, QSharedPointer( new RespData( 666 ) ))); QTest::newRow("appenduid-simple") << QByteArray("A003 OK [APPENDUID 38505 3955] APPEND completed\r\n") << QSharedPointer(new State("A003", OK, QStringLiteral("APPEND completed"), APPENDUID, QSharedPointer( new RespData >( qMakePair(38505u, Imap::Sequence(3955))) ))); /* QTest::newRow("appenduid-seq") << QByteArray("A003 OK [APPENDUID 38505 3955,333666] APPEND completed\r\n") << QSharedPointer(new State("A003", OK, "APPEND completed", APPENDUID, QSharedPointer( new RespData >( qMakePair(38505u, Imap::Sequence(3955))) ))); QTest::newRow("copyuid-simple") << QByteArray("A004 OK [COPYUID 38505 304 3956] Done\r\n") << QSharedPointer(new State("A004", OK, "Done", COPYUID, QSharedPointer( new RespData > >( qMakePair(38505u, qMakePair(Imap::Sequence(304), Imap::Sequence(3956)) )) ))); QTest::newRow("copyuid-sequence") << QByteArray("A004 OK [COPYUID 38505 304,319:320 3956:3958] Done\r\n") << QSharedPointer(new State("A003", OK, "Done", APPENDUID, QSharedPointer( new RespData >( qMakePair(38505u, Imap::Sequence(3955))) ))); */ } /** @short Test untagged response parsing */ void ImapParserParseTest::testParseUntagged() { QFETCH( QByteArray, line ); QFETCH( QSharedPointer, response ); Q_ASSERT( response ); QSharedPointer r = parser->parseUntagged( line ); if ( Imap::Responses::Fetch* fetchResult = dynamic_cast( r.data() ) ) { fetchResult->data.remove( "x-trojita-bodystructure" ); } #if 0// qDebug()'s internal buffer is too small to be useful here, that's why QCOMPARE's normal dumping is not enough if ( *r != *response ) { QTextStream s( stderr ); s << "\nPARSED:\n" << *r; s << "\nEXPETCED:\n"; s << *response; } #endif QCOMPARE( *r, *response ); } void ImapParserParseTest::testParseUntagged_data() { using namespace Imap::Responses; using namespace Imap::Message; using Imap::Responses::Fetch; // needed for gcc-3.4 QTest::addColumn("line"); QTest::addColumn >("response"); QSharedPointer voidData( new RespData() ); QSharedPointer emptyList( new RespData( QStringList() ) ); QTest::newRow("untagged-ok-simple") << QByteArray("* OK Everything works, man!\r\n") << QSharedPointer( new State(QByteArray(), OK, QStringLiteral("Everything works, man!"), NONE, voidData ) ); QTest::newRow("untagged-no-simple") << QByteArray("* NO Nope, something is broken\r\n") << QSharedPointer( new State(QByteArray(), NO, QStringLiteral("Nope, something is broken"), NONE, voidData ) ); QTest::newRow("untagged-ok-uidvalidity") << QByteArray("* OK [UIDVALIDITY 17] UIDs valid\r\n") << QSharedPointer( new State(QByteArray(), OK, QStringLiteral("UIDs valid"), UIDVALIDITY, QSharedPointer( new RespData( 17 ) ))); QTest::newRow("untagged-bye") << QByteArray("* BYE go away\r\n") << QSharedPointer( new State(QByteArray(), BYE, QStringLiteral("go away"), NONE, voidData ) ); QTest::newRow("untagged-bye-empty") << QByteArray("* BYE\r\n") << QSharedPointer( new State(QByteArray(), BYE, QString(), NONE, voidData ) ); QTest::newRow("untagged-no-somerespcode-empty") << QByteArray("* NO [ALERT]\r\n") << QSharedPointer( new State(QByteArray(), NO, QString(), ALERT, voidData ) ); QTest::newRow("untagged-expunge") << QByteArray("* 1337 Expunge\r\n") << QSharedPointer( new NumberResponse( EXPUNGE, 1337 ) ); QTest::newRow("untagged-exists") << QByteArray("* 3 exIsts\r\n") << QSharedPointer( new NumberResponse( EXISTS, 3 ) ); QTest::newRow("untagged-recent") << QByteArray("* 666 recenT\r\n") << QSharedPointer( new NumberResponse( RECENT, 666 ) ); QTest::newRow("untagged-capability") << QByteArray("* CAPABILITY fooBar IMAP4rev1 blah\r\n") << QSharedPointer( new Capability( QStringList() << QStringLiteral("fooBar") << QStringLiteral("IMAP4rev1") << QStringLiteral("blah") ) ); QTest::newRow("untagged-list") << QByteArray("* LIST (\\Noselect) \".\" \"\"\r\n") << QSharedPointer( new List( LIST, QStringList() << QStringLiteral("\\Noselect"), QStringLiteral("."), QLatin1String(""), QMap()) ); QTest::newRow("untagged-lsub") << QByteArray("* LSUB (\\Noselect) \".\" \"\"\r\n") << QSharedPointer( new List( LSUB, QStringList() << QStringLiteral("\\Noselect"), QStringLiteral("."), QLatin1String(""), QMap() ) ); QTest::newRow("untagged-list-moreflags") << QByteArray("* LIST (\\Noselect Blesmrt) \".\" \"\"\r\n") << QSharedPointer( new List( LIST, QStringList() << QStringLiteral("\\Noselect") << QStringLiteral("Blesmrt"), QStringLiteral("."), QLatin1String(""), QMap() ) ); QTest::newRow("untagged-list-mailbox") << QByteArray("* LIST () \".\" \"someName\"\r\n") << QSharedPointer( new List( LIST, QStringList(), QStringLiteral("."), QStringLiteral("someName"), QMap() ) ); QTest::newRow("untagged-list-mailbox-atom") << QByteArray("* LIST () \".\" someName\r\n") << QSharedPointer( new List( LIST, QStringList(), QStringLiteral("."), QStringLiteral("someName"), QMap() ) ); QTest::newRow("untagged-list-separator-nil") << QByteArray("* LIST () NiL someName\r\n") << QSharedPointer( new List( LIST, QStringList(), QByteArray(), QStringLiteral("someName"), QMap() ) ); QTest::newRow("untagged-list-mailbox-quote") << QByteArray("* LIST () \".\" \"some\\\"Name\"\r\n") << QSharedPointer( new List( LIST, QStringList(), QStringLiteral("."), QStringLiteral("some\"Name"), QMap() ) ); QTest::newRow("untagged-list-unicode") << QByteArray("* LIST () \"/\" \"~Peter/mail/&U,BTFw-/&ZeVnLIqe-\"\r\n") << QSharedPointer( new List( LIST, QStringList(), QStringLiteral("/"), QStringLiteral("~Peter/mail/台北/日本語"), QMap() ) ); QTest::newRow("untagged-list-inbox-atom") << QByteArray("* LIST () \"/\" inBox\r\n") << QSharedPointer( new List( LIST, QStringList(), QStringLiteral("/"), QStringLiteral("INBOX"), QMap()) ); QTest::newRow("untagged-list-inbox-quoted") << QByteArray("* LIST () \"/\" \"inBox\"\r\n") << QSharedPointer( new List( LIST, QStringList(), QStringLiteral("/"), QStringLiteral("INBOX"), QMap()) ); QTest::newRow("untagged-list-inbox-literal") << QByteArray("* LIST () \"/\" {5}\r\ninBox\r\n") << QSharedPointer( new List( LIST, QStringList(), QStringLiteral("/"), QStringLiteral("INBOX"), QMap()) ); QTest::newRow("untagged-list-numeric-mailbox") << QByteArray("* LIST () \"/\" 666\r\n") << QSharedPointer(new List(LIST, QStringList(), QStringLiteral("/"), QStringLiteral("666"), QMap())); QTest::newRow("untagged-list-numeric-mixed-mailbox") << QByteArray("* LIST () \"/\" 666x333\r\n") << QSharedPointer(new List(LIST, QStringList(), QStringLiteral("/"), QStringLiteral("666x333"), QMap())); // https://bugs.kde.org/show_bug.cgi?id=334456 QTest::newRow("untagged-list-groupwise-bug-334456") << QByteArray("* LIST (\\Unmarked) \"/\" \"Calendar/GroupWise.5-Mehrfachbenutzer \\(Standard\\).MultiUser Control\"\r\n") << QSharedPointer(new List(LIST, QStringList() << QStringLiteral("\\Unmarked"), QStringLiteral("/"), QStringLiteral("Calendar/GroupWise.5-Mehrfachbenutzer (Standard).MultiUser Control"), QMap())); QMap listExtData; listExtData["CHILDINFO"] = QStringList() << QStringLiteral("SUBSCRIBED"); QTest::newRow("untagged-list-ext-childinfo") << QByteArray("* LIST () \"/\" \"Foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n") << QSharedPointer(new List( LIST, QStringList(), QStringLiteral("/"), QStringLiteral("Foo"), listExtData)); listExtData["OLDNAME"] = QStringList() << QStringLiteral("blesmrt"); QTest::newRow("untagged-list-ext-childinfo-2") << QByteArray("* LIST () \"/\" \"Foo\" (\"CHILDINFO\" (\"SUBSCRIBED\") \"OLDNAME\" (\"blesmrt\"))\r\n") << QSharedPointer(new List( LIST, QStringList(), QStringLiteral("/"), QStringLiteral("Foo"), listExtData)); QTest::newRow("untagged-flags") << QByteArray("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n") << QSharedPointer( new Flags( QStringList() << QStringLiteral("\\Answered") << QStringLiteral("\\Flagged") << QStringLiteral("\\Deleted") << QStringLiteral("\\Seen") << QStringLiteral("\\Draft") ) ); QTest::newRow("untagged-flags-numeric") << QByteArray("* FLAGS (333 666x333)\r\n") << QSharedPointer(new Flags(QStringList() << QStringLiteral("333") << QStringList(QStringLiteral("666x333")))); QTest::newRow("search-empty") << QByteArray("* SEARCH\r\n") << QSharedPointer(new Search(Imap::Uids())); QTest::newRow("search-messages") << QByteArray("* SEARCH 1 33 666\r\n") << QSharedPointer(new Search(Imap::Uids() << 1 << 33 << 666)); ESearch::ListData_t esearchData; QTest::newRow("esearch-empty") << QByteArray("* ESEARCH\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); QTest::newRow("esearch-empty-tag") << QByteArray("* ESEARCH (TAG x)\r\n") << QSharedPointer(new ESearch("x", ESearch::SEQUENCE, esearchData)); QTest::newRow("esearch-empty-uid") << QByteArray("* ESEARCH UiD\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::UIDS, esearchData)); QTest::newRow("esearch-empty-uid-tag") << QByteArray("* ESEARCH (TAG \"1\") UiD\r\n") << QSharedPointer(new ESearch("1", ESearch::UIDS, esearchData)); esearchData.push_back(qMakePair<>(QByteArray("BLAH"), Imap::Uids() << 10)); QTest::newRow("esearch-one-number") << QByteArray("* ESEARCH BLaH 10\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); QTest::newRow("esearch-uid-one-number") << QByteArray("* ESEArCH UiD BLaH 10\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::UIDS, esearchData)); QTest::newRow("esearch-tag-one-number") << QByteArray("* ESEArCH (TaG x) BLaH 10\r\n") << QSharedPointer(new ESearch("x", ESearch::SEQUENCE, esearchData)); QTest::newRow("esearch-uid-tag-one-number") << QByteArray("* ESEArCH (TaG x) Uid BLaH 10\r\n") << QSharedPointer(new ESearch("x", ESearch::UIDS, esearchData)); esearchData.push_front(qMakePair<>(QByteArray("FOO"), Imap::Uids() << 666)); esearchData.push_front(qMakePair<>(QByteArray("FOO"), Imap::Uids() << 333)); QTest::newRow("esearch-two-numbers") << QByteArray("* ESEARCH fOO 333 foo 666 BLaH 10\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); esearchData.clear(); esearchData.push_back(qMakePair<>(QByteArray("FOO"), Imap::Uids() << 333)); esearchData.push_back(qMakePair<>(QByteArray("BLAH"), Imap::Uids() << 10)); QTest::newRow("esearch-uid-two-numbers") << QByteArray("* ESEArCH UiD foo 333 BLaH 10\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::UIDS, esearchData)); QTest::newRow("esearch-tag-two-numbers") << QByteArray("* ESEArCH (TaG x) foo 333 BLaH 10 \r\n") << QSharedPointer(new ESearch("x", ESearch::SEQUENCE, esearchData)); QTest::newRow("esearch-uid-tag-two-numbers") << QByteArray("* ESEArCH (TaG x) Uid foo 333 BLaH 10\r\n") << QSharedPointer(new ESearch("x", ESearch::UIDS, esearchData)); esearchData.clear(); esearchData.push_back(qMakePair<>(QByteArray("BLAH"), Imap::Uids() << 10 << 11 << 13 << 14 << 15 << 16 << 17)); QTest::newRow("esearch-one-list-1") << QByteArray("* ESEARCH BLaH 10,11,13:17\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); esearchData.clear(); esearchData.push_back(qMakePair<>(QByteArray("BLAH"), Imap::Uids() << 1 << 2)); QTest::newRow("esearch-one-list-2") << QByteArray("* ESEARCH BLaH 1:2\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); QTest::newRow("esearch-one-list-3") << QByteArray("* ESEARCH BLaH 1,2\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); esearchData.clear(); esearchData.push_back(qMakePair<>(QByteArray("BLAH"), Imap::Uids() << 1 << 2 << 3 << 4 << 5)); QTest::newRow("esearch-one-list-4") << QByteArray("* ESEARCH BLaH 1,2:4,5\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); QTest::newRow("esearch-one-list-extra-space-at-end") << QByteArray("* ESEARCH BLaH 1,2:4,5 \r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); esearchData.push_back(qMakePair<>(QByteArray("FOO"), Imap::Uids() << 6)); QTest::newRow("esearch-mixed-1") << QByteArray("* ESEARCH BLaH 1,2:4,5 FOO 6\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); std::swap(esearchData[0], esearchData[1]); QTest::newRow("esearch-mixed-2") << QByteArray("* ESEARCH FOO 6 BLaH 1,2:4,5\r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); esearchData.push_back(qMakePair<>(QByteArray("BAZ"), Imap::Uids() << 33)); QTest::newRow("esearch-mixed-3") << QByteArray("* ESEARCH FOO 6 BLaH 1,2:4,5 baz 33 \r\n") << QSharedPointer(new ESearch(QByteArray(), ESearch::SEQUENCE, esearchData)); ESearch::IncrementalContextData_t incrementalEsearchData; incrementalEsearchData.push_back(ESearch::ContextIncrementalItem(ESearch::ContextIncrementalItem::ADDTO, 1, Imap::Uids() << 2733)); incrementalEsearchData.push_back(ESearch::ContextIncrementalItem(ESearch::ContextIncrementalItem::ADDTO, 1, Imap::Uids() << 2731 << 2732)); QTest::newRow("esearch-context-sort-1") << QByteArray("* ESEARCH (TAG \"C01\") UID ADDTO (1 2733) ADDTO (1 2731:2732)\r\n") << QSharedPointer(new ESearch("C01", ESearch::UIDS, incrementalEsearchData)); QTest::newRow("esearch-context-sort-2") << QByteArray("* ESEARCH (TAG \"C01\") UID ADDTO (1 2733 1 2731:2732)\r\n") << QSharedPointer(new ESearch("C01", ESearch::UIDS, incrementalEsearchData)); incrementalEsearchData.clear(); incrementalEsearchData.push_back(ESearch::ContextIncrementalItem(ESearch::ContextIncrementalItem::ADDTO, 1, Imap::Uids() << 2733)); incrementalEsearchData.push_back(ESearch::ContextIncrementalItem(ESearch::ContextIncrementalItem::ADDTO, 1, Imap::Uids() << 2732)); incrementalEsearchData.push_back(ESearch::ContextIncrementalItem(ESearch::ContextIncrementalItem::ADDTO, 1, Imap::Uids() << 2731)); QTest::newRow("esearch-context-sort-3") << QByteArray("* ESEARCH (TAG \"C01\") UID ADDTO (1 2733 1 2732 1 2731)\r\n") << QSharedPointer(new ESearch("C01", ESearch::UIDS, incrementalEsearchData)); incrementalEsearchData.clear(); incrementalEsearchData.push_back(ESearch::ContextIncrementalItem(ESearch::ContextIncrementalItem::ADDTO, 1, Imap::Uids() << 2731 << 2732 << 2733)); QTest::newRow("esearch-context-sort-4") << QByteArray("* ESEARCH (TAG \"C01\") UID ADDTO (1 2731:2733)\r\n") << QSharedPointer(new ESearch("C01", ESearch::UIDS, incrementalEsearchData)); incrementalEsearchData.clear(); incrementalEsearchData.push_back(ESearch::ContextIncrementalItem(ESearch::ContextIncrementalItem::REMOVEFROM, 0, Imap::Uids() << 32768)); QTest::newRow("esearch-context-sort-5") << QByteArray("* ESEARCH (TAG \"B01\") UID REMOVEFROM (0 32768)\r\n") << QSharedPointer(new ESearch("B01", ESearch::UIDS, incrementalEsearchData)); Status::stateDataType states; states[Status::MESSAGES] = 231; states[Status::UIDNEXT] = 44292; QTest::newRow("status-1") << QByteArray("* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)\r\n") << QSharedPointer( new Status( QStringLiteral("blurdybloop"), states ) ); QTest::newRow("status-numeric-only") << QByteArray("* STATUS 333 (MESSAGES 231 UIDNEXT 44292)\r\n") << QSharedPointer(new Status(QStringLiteral("333"), states)); QTest::newRow("status-numeric-mixed") << QByteArray("* STATUS 333x666 (MESSAGES 231 UIDNEXT 44292)\r\n") << QSharedPointer(new Status(QStringLiteral("333x666"), states)); states[Status::UIDVALIDITY] = 1337; states[Status::RECENT] = 3234567890u; QTest::newRow("status-2") << QByteArray("* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292 UIDVALIDITY 1337 RECENT 3234567890)\r\n") << QSharedPointer( new Status( QStringLiteral("blurdybloop"), states ) ); // notice the trailing whitespace QTest::newRow("status-exchange-botched-1") << QByteArray("* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292 UIDVALIDITY 1337 RECENT 3234567890) \r\n") << QSharedPointer( new Status( QStringLiteral("blurdybloop"), states ) ); states.clear(); states[Status::MESSAGES] = 113; QTest::newRow("status-extra-whitespace-around-atoms") << QByteArray("* STATUS blurdybloop ( MESSAGES 113 )\r\n") << QSharedPointer( new Status( QStringLiteral("blurdybloop"), states ) ); // https://bugs.kde.org/show_bug.cgi?id=365149 states.clear(); states[Status::MESSAGES] = 702; QTest::newRow("untagged-extra-space-before-status") << QByteArray("* STATUS \"INBOX\" (MESSAGES 702)\r\n") << QSharedPointer(new Status(QStringLiteral("INBOX"), states)); QTest::newRow("namespace-1") << QByteArray("* Namespace nil NIL nil\r\n") << QSharedPointer( new Namespace( QList(), QList(), QList() ) ); QTest::newRow("namespace-2") << QByteArray("* Namespace () NIL nil\r\n") << QSharedPointer( new Namespace( QList(), QList(), QList() ) ); QTest::newRow("namespace-3") << QByteArray("* Namespace ((\"\" \"/\")) NIL nil\r\n") << QSharedPointer( new Namespace( QList() << NamespaceData( QLatin1String(""), QStringLiteral("/") ), QList(), QList() ) ); QTest::newRow("namespace-4") << QByteArray("* Namespace ((\"prefix\" \".\")(\"\" \"/\")) ((\"blesmrt\" \"trojita\")) ((\"foo\" \"bar\"))\r\n") << QSharedPointer( new Namespace( QList() << NamespaceData( QStringLiteral("prefix"), QStringLiteral(".") ) << NamespaceData( QLatin1String(""), QStringLiteral("/") ), QList() << NamespaceData( QStringLiteral("blesmrt"), QStringLiteral("trojita") ), QList() << NamespaceData( QStringLiteral("foo"), QStringLiteral("bar") ) ) ); QTest::newRow("namespace-5") << QByteArray("* NAMESPACE ((\"\" \"/\")(\"#mhinbox\" NIL)(\"#mh/\" \"/\")) ((\"~\" \"/\")) " "((\"#shared/\" \"/\")(\"#ftp/\" \"/\")(\"#news.\" \".\")(\"#public/\" \"/\"))\r\n") << QSharedPointer( new Namespace( QList() << NamespaceData( QLatin1String(""), QStringLiteral("/") ) << NamespaceData( QStringLiteral("#mhinbox"), QByteArray() ) << NamespaceData( QStringLiteral("#mh/"), QStringLiteral("/") ), QList() << NamespaceData( QStringLiteral("~"), QStringLiteral("/") ), QList() << NamespaceData( QStringLiteral("#shared/"), QStringLiteral("/") ) << NamespaceData( QStringLiteral("#ftp/"), QStringLiteral("/") ) << NamespaceData( QStringLiteral("#news."), QStringLiteral(".") ) << NamespaceData( QStringLiteral("#public/"), QStringLiteral("/") ) ) ); QTest::newRow("sort-1") << QByteArray("* SORT") << QSharedPointer(new Sort(Imap::Uids())); QTest::newRow("sort-2") << QByteArray("* SORT ") << QSharedPointer(new Sort(Imap::Uids())); QTest::newRow("sort-3") << QByteArray("* SORT 13 1 6 5 7 9 10 11 12 4 3 2 8\r\n") << QSharedPointer(new Sort(Imap::Uids() << 13 << 1 << 6 << 5 << 7 << 9 << 10 << 11 << 12 << 4 << 3 << 2 << 8)); ThreadingNode node2(2), node3(3), node6(6), node4(4), node23(23), node44(44), node7(7), node96(96); QVector rootNodes; node4.children << node23; node7.children << node96; node44.children << node7; node6.children << node4 << node44; node3.children << node6; rootNodes << node2 << node3; QTest::newRow("thread-1") << QByteArray("* THREAD (2)(3 6 (4 23)(44 7 96))\r\n") << QSharedPointer( new Thread( rootNodes ) ); rootNodes.clear(); ThreadingNode node203(3), node205(5), anonymousNode201; anonymousNode201.children << node203 << node205; rootNodes << anonymousNode201; QTest::newRow("thread-2") << QByteArray("* THREAD ((3)(5))\r\n") << QSharedPointer( new Thread( rootNodes ) ); rootNodes.clear(); ThreadingNode node301(1), node302(2), node303(3); rootNodes << node301 << node302 << node303; QTest::newRow("thread-3") << QByteArray("* THREAD (1)(2)(3)\r\n") << QSharedPointer( new Thread( rootNodes ) ); rootNodes.clear(); ThreadingNode node401(1), node402(2), node403(3); node401.children << node402 << node403; rootNodes << node401; QTest::newRow("thread-4") << QByteArray("* THREAD (1(2)(3))\r\n") << QSharedPointer( new Thread( rootNodes ) ); rootNodes.clear(); ThreadingNode node502(2), node503(3), node506(6), node504(4), node523(23), node544(44), node507(7), node596(96), node513(13), node566(66), anon501, anon502; node504.children << node523; node507.children << node596; node544.children << node507; anon502.children << node566; anon501.children << anon502; node506.children << node504 << node544 << node513 << anon501; node503.children << node506; rootNodes << node502 << node503; QTest::newRow("thread-5") << QByteArray("* THREAD (2)(3 6 (4 23)(44 7 96) (13) (((66))))\r\n") << QSharedPointer( new Thread( rootNodes ) ); rootNodes.clear(); ThreadingNode node608(8), node602(2), node603(3), node604(4), node607(7), node609(9), node610(10), node611(11), node612(12), node605(5), node601(1), node606(6), node613(13); node603.children << node604; node602.children << node603; rootNodes << node608 << node602 << node607 << node609 << node610; rootNodes << ThreadingNode(0, QVector() << node611 << node612 ); rootNodes << node605; rootNodes << ThreadingNode(0, QVector() << node601 << node606 ); rootNodes << node613; QTest::newRow("thread-6") << QByteArray("* THREAD (8)(2 3 4)(7)(9)(10)((11)(12))(5)((1)(6))(13)\r\n") << QSharedPointer( new Thread( rootNodes ) ); esearchData.clear(); ESearch::IncrementalThreadingData_t esearchThreading; esearchThreading.push_back(ESearch::IncrementalThreadingItem_t(666, rootNodes)); QTest::newRow("esearch-incthread-single") << QByteArray("* ESEARCH (TAG \"x\") UID INCTHREAD 666 (8)(2 3 4)(7)(9)(10)((11)(12))(5)((1)(6))(13)\r\n") << QSharedPointer(new ESearch("x", ESearch::UIDS, esearchThreading)); rootNodes.clear(); rootNodes << ThreadingNode(123, QVector()); esearchThreading.push_back(ESearch::IncrementalThreadingItem_t(333, rootNodes)); QTest::newRow("esearch-incthread-two") << QByteArray("* ESEARCH (TAG \"x\") UID INCTHREAD 666 (8)(2 3 4)(7)(9)(10)((11)(12))(5)((1)(6))(13) " "INCTHREAD 333 (123)\r\n") << QSharedPointer(new ESearch("x", ESearch::UIDS, esearchThreading)); Fetch::dataType fetchData; QTest::newRow("fetch-empty") << QByteArray("* 66 FETCH ()\r\n") << QSharedPointer( new Fetch( 66, fetchData ) ); fetchData.clear(); fetchData[ "RFC822.SIZE" ] = QSharedPointer( new RespData( 1337 ) ); QTest::newRow("fetch-rfc822-size") << QByteArray("* 123 FETCH (rfc822.size 1337)\r\n") << QSharedPointer( new Fetch( 123, fetchData ) ); fetchData.clear(); fetchData[ "RFC822.SIZE" ] = QSharedPointer( new RespData( 1337 ) ); fetchData[ "UID" ] = QSharedPointer( new RespData( 666 ) ); QTest::newRow("fetch-rfc822-size-uid") << QByteArray("* 123 FETCH (uID 666 rfc822.size 1337)\r\n") << QSharedPointer( new Fetch( 123, fetchData ) ); QTest::newRow("fetch-rfc822-size-uid-swapped") << QByteArray("* 123 FETCH (rfc822.size 1337 uId 666)\r\n") << QSharedPointer( new Fetch( 123, fetchData ) ); fetchData.clear(); fetchData[ "RFC822.HEADER" ] = QSharedPointer( new RespData( "123456789012" ) ); QTest::newRow("fetch-rfc822-header") << QByteArray("* 123 FETCH (rfc822.header {12}\r\n123456789012)\r\n") << QSharedPointer( new Fetch( 123, fetchData ) ); fetchData.clear(); fetchData[ "RFC822.TEXT" ] = QSharedPointer( new RespData( "abcdEf" ) ); QTest::newRow("fetch-rfc822-text") << QByteArray("* 123 FETCH (rfc822.tExt abcdEf)\r\n") << QSharedPointer( new Fetch( 123, fetchData ) ); fetchData.clear(); fetchData[ "INTERNALDATE" ] = QSharedPointer( new RespData( QDateTime( QDate(2007, 3, 7), QTime( 14, 3, 32 ), Qt::UTC ) ) ); QTest::newRow("fetch-rfc822-internaldate") << QByteArray("* 123 FETCH (InternalDate \"07-Mar-2007 15:03:32 +0100\")\r\n") << QSharedPointer( new Fetch( 123, fetchData ) ); fetchData.clear(); fetchData[ "INTERNALDATE" ] = QSharedPointer( new RespData( QDateTime( QDate(1981, 4, 6), QTime( 18, 33, 32 ), Qt::UTC ) ) ); QTest::newRow("fetch-rfc822-internaldate-shorter") << QByteArray("* 123 FETCH (InternalDate \"6-Apr-1981 12:03:32 -0630\")\r\n") << QSharedPointer( new Fetch( 123, fetchData ) ); fetchData.clear(); QDateTime date( QDate( 1996, 7, 17 ), QTime( 9, 23, 25 ), Qt::UTC ); QString subject( QStringLiteral("IMAP4rev1 WG mtg summary and minutes")); QList from, sender, replyTo, to, cc, bcc; from.append( MailAddress( QStringLiteral("Terry Gray"), QByteArray(), QStringLiteral("gray"), QStringLiteral("cac.washington.edu")) ); sender = replyTo = from; to.append( MailAddress( QByteArray(), QByteArray(), QStringLiteral("imap"), QStringLiteral("cac.washington.edu")) ); cc.append( MailAddress( QByteArray(), QByteArray(), QStringLiteral("minutes"), QStringLiteral("CNRI.Reston.VA.US")) ); cc.append( MailAddress( QStringLiteral("John Klensin"), QByteArray(), QStringLiteral("KLENSIN"), QStringLiteral("MIT.EDU")) ); QByteArray messageId( "B27397-0100000@cac.washington.edu" ); fetchData[ "ENVELOPE" ] = QSharedPointer( new RespData( Envelope( date, subject, from, sender, replyTo, to, cc, bcc, QList(), messageId ) ) ); QTest::newRow("fetch-envelope") << QByteArray( "* 12 FETCH (ENVELOPE (\"Wed, 17 Jul 1996 02:23:25 -0700 (PDT)\" " "\"IMAP4rev1 WG mtg summary and minutes\" " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((NIL NIL \"imap\" \"cac.washington.edu\")) " "((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") " "(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL " "\"\"))\r\n" ) << QSharedPointer( new Fetch( 12, fetchData ) ); fetchData.clear(); fetchData[ "ENVELOPE" ] = QSharedPointer( new RespData( Envelope( QDateTime(), subject, from, sender, replyTo, to, cc, bcc, QList(), messageId ) ) ); QTest::newRow("fetch-envelope-nildate") << QByteArray( "* 13 FETCH (ENVELOPE (NIL " "\"IMAP4rev1 WG mtg summary and minutes\" " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((NIL NIL \"imap\" \"cac.washington.edu\")) " "((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") " "(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL " "\"\"))\r\n" ) << QSharedPointer( new Fetch( 13, fetchData ) ); // FIXME: more unit tests for ENVELOPE and BODY[ fetchData.clear(); AbstractMessage::bodyFldParam_t bodyFldParam; AbstractMessage::bodyFldDsp_t bodyFldDsp; bodyFldParam[ "CHARSET" ] = "UTF-8"; bodyFldParam[ "FORMAT" ] = "flowed"; fetchData[ "BODYSTRUCTURE" ] = QSharedPointer( new TextMessage( "text", "plain", bodyFldParam, QByteArray(), QByteArray(), "8bit", 362, QByteArray(), bodyFldDsp, QList(), QByteArray(), QVariant(), 15 ) ); QTest::newRow("fetch-bodystructure-plain") << QByteArray( "* 3 FETCH (BODYSTRUCTURE (\"text\" \"plain\" (\"chaRset\" \"UTF-8\" \"format\" \"flowed\") NIL NIL \"8bit\" 362 15 NIL NIL NIL))\r\n" ) << QSharedPointer( new Fetch( 3, fetchData ) ); fetchData.clear(); QList > msgList; // 1.1 bodyFldParam.clear(); bodyFldDsp = AbstractMessage::bodyFldDsp_t(); bodyFldParam[ "CHARSET" ] = "US-ASCII"; bodyFldParam[ "DELSP" ] = "yes"; bodyFldParam[ "FORMAT" ] = "flowed"; msgList.append( QSharedPointer( new TextMessage( "text", "plain", bodyFldParam, QByteArray(), QByteArray(), "7bit", 990, QByteArray(), bodyFldDsp, QList(), QByteArray(), QVariant(), 27 ) ) ); // 1.2 bodyFldParam.clear(); bodyFldParam[ "X-MAC-TYPE" ] = "70674453"; bodyFldParam[ "NAME" ] = "PGP.sig"; bodyFldDsp = AbstractMessage::bodyFldDsp_t(); bodyFldDsp.first = "inline"; bodyFldDsp.second[ "FILENAME" ] = "PGP.sig"; msgList.append( QSharedPointer( new TextMessage( "application", "pgp-signature", bodyFldParam, QByteArray(), "This is a digitally signed message part", "7bit", 193, QByteArray(), bodyFldDsp, QList(), QByteArray(), QVariant(), 0 ) ) ); // 1 bodyFldParam.clear(); bodyFldParam[ "PROTOCOL" ] = "application/pgp-signature"; bodyFldParam[ "MICALG" ] = "pgp-sha1"; bodyFldParam[ "BOUNDARY" ] = "Apple-Mail-10--856231115"; bodyFldDsp = AbstractMessage::bodyFldDsp_t(); fetchData[ "BODYSTRUCTURE" ] = QSharedPointer( new MultiMessage( msgList, "signed", bodyFldParam, bodyFldDsp, QList(), QByteArray(), QVariant() ) ); QTest::newRow("fetch-bodystructure-signed") << QByteArray("* 1 FETCH (BODYSTRUCTURE ((\"text\" \"plain\" (\"charset\" \"US-ASCII\" \"delsp\" \"yes\" \"format\" \"flowed\") NIL NIL \"7bit\" 990 27 NIL NIL NIL)(\"application\" \"pgp-signature\" (\"x-mac-type\" \"70674453\" \"name\" \"PGP.sig\") NIL \"This is a digitally signed message part\" \"7bit\" 193 NIL (\"inline\" (\"filename\" \"PGP.sig\")) NIL) \"signed\" (\"protocol\" \"application/pgp-signature\" \"micalg\" \"pgp-sha1\" \"boundary\" \"Apple-Mail-10--856231115\") NIL NIL))\r\n") << QSharedPointer( new Fetch( 1, fetchData ) ); fetchData.clear(); bodyFldParam.clear(); bodyFldParam[ "CHARSET" ] = "UTF-8"; bodyFldParam[ "FORMAT" ] = "flowed"; fetchData[ "BODYSTRUCTURE" ] = QSharedPointer( new TextMessage( "text", "plain", bodyFldParam, QByteArray(), QByteArray(), "quoted-printable", 0, QByteArray(), bodyFldDsp, QList(), QByteArray(), QVariant(), 0 ) ); QTest::newRow("fetch-exchange-screwup-1") << QByteArray("* 61 FETCH " "(BODYSTRUCTURE (\"text\" \"plain\" (\"charset\" \"UTF-8\" \"format\" \"flowed\") " "NIL NIL \"quoted-printable\" -1 -1 NIL NIL NIL NIL))\r\n") << QSharedPointer( new Fetch( 61, fetchData ) ); msgList.clear(); bodyFldParam.clear(); bodyFldDsp = AbstractMessage::bodyFldDsp_t(); bodyFldParam["CHARSET"] = "utf-8"; msgList.append(QSharedPointer(new TextMessage("text", "plain", bodyFldParam, QByteArray(), QByteArray(), "quoted-printable", 333, QByteArray(), bodyFldDsp, QList(), QByteArray(), QVariant(), 10))); msgList.append(QSharedPointer(new TextMessage("text", "html", bodyFldParam, QByteArray(), QByteArray(), "7bit", 666, QByteArray(), bodyFldDsp, QList(), QByteArray(), QVariant(), 20))); fetchData.clear(); bodyFldParam.clear(); bodyFldParam["BOUNDARY"] = "=_1234"; fetchData["BODYSTRUCTURE"] = QSharedPointer(new MultiMessage(msgList, "alternative", bodyFldParam, bodyFldDsp, QList(), QByteArray(), QVariant())); // Exchange sends utter crap inside the body-fld-dsp, unfortunately. // This was reported thhrough an encrypted mail in a followup to https://bugs.kde.org/show_bug.cgi?id=334056, // but the problem is unrelted to the original topic of that bug. QTest::newRow("fetch-exchange-screwup-bodystructure-body-fld-dsp") << QByteArray("* 1 FETCH (BODYSTRUCTURE (" "(\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"quoted-printable\" 333 10 NIL NIL NIL NIL)" "(\"text\" \"html\" (\"charset\" \"utf-8\") NIL NIL \"7bit\" 666 20 NIL NIL NIL NIL) " "\"alternative\" (\"boundary\" \"=_1234\") \"random@exchange!crap!goes!here!\" NIL))\r\n") << QSharedPointer(new Fetch(1, fetchData)); fetchData.clear(); bodyFldParam.clear(); bodyFldDsp = AbstractMessage::bodyFldDsp_t(); bodyFldDsp.first = "inline"; bodyFldParam["CHARSET"] = "utf-8"; fetchData["BODYSTRUCTURE"] = QSharedPointer( new TextMessage("text", "html", bodyFldParam, QByteArray(), QByteArray(), "quoted-printable", 4848, QByteArray(), bodyFldDsp, QList(), QByteArray(), QVariant(), 25)); QTest::newRow("fetch-body-fld-dsp-inline-citadel") << QByteArray("* 12 FETCH (BODYSTRUCTURE (\"text\" \"html\" (\"CHARSET\" \"utf-8\") NIL NIL \"quoted-printable\" 4848 25 NIL (\"inline\") NIL))\r\n") << QSharedPointer(new Fetch(12, fetchData)); // GMail and its flawed representation of a nested message/rfc822 fetchData.clear(); from.clear(); from << MailAddress(QStringLiteral("somebody"), QString(), QStringLiteral("info"), QStringLiteral("example.com")); sender = replyTo = from; to.clear(); to << MailAddress(QStringLiteral("destination"), QString(), QStringLiteral("foobar"), QStringLiteral("gmail.com")); cc.clear(); bcc.clear(); fetchData["ENVELOPE"] = QSharedPointer( new RespData( Envelope( QDateTime(QDate(2011, 1, 11), QTime(9, 21, 42), Qt::UTC), QStringLiteral("blablabla"), from, sender, replyTo, to, cc, bcc, QList(), QByteArray() ) )); fetchData["UID"] = QSharedPointer(new RespData(8803)); fetchData["RFC822.SIZE"] = QSharedPointer(new RespData(56144)); msgList.clear(); bodyFldParam.clear(); bodyFldParam["CHARSET"] = "iso-8859-2"; msgList.append(QSharedPointer( new TextMessage("text", "plain", bodyFldParam, QByteArray(), QByteArray(), "QUOTED-PRINTABLE", 52, QByteArray(), AbstractMessage::bodyFldDsp_t(), QList(), QByteArray(), QVariant(), 2))); msgList.append(QSharedPointer( new TextMessage("text", "html", bodyFldParam, QByteArray(), QByteArray(), "QUOTED-PRINTABLE", 1739, QByteArray(), AbstractMessage::bodyFldDsp_t(), QList(), QByteArray(), QVariant(), 66))); bodyFldParam.clear(); bodyFldParam["BOUNDARY"] = "----=_NextPart_001_0078_01CBB179.57530990"; msgList = QList >() << QSharedPointer( new MultiMessage( msgList, "alternative", bodyFldParam, AbstractMessage::bodyFldDsp_t(), QList(), QByteArray(), QVariant() ) ); msgList << QSharedPointer( new MsgMessage("message", "rfc822", AbstractMessage::bodyFldParam_t(), QByteArray(), QByteArray(), "7BIT", 836, QByteArray(), AbstractMessage::bodyFldDsp_t(), QList(), QByteArray(), QVariant(), Envelope(), QSharedPointer( new BasicMessage("attachment", QByteArray(), AbstractMessage::bodyFldParam_t(), QByteArray(), QByteArray(), QByteArray(), 0, QByteArray(), AbstractMessage::bodyFldDsp_t(), QList(), QByteArray(), QVariant()) ), 0 )); msgList << QSharedPointer( new MsgMessage("message", "rfc822", AbstractMessage::bodyFldParam_t(), QByteArray(), QByteArray(), "7BIT", 50785, QByteArray(), AbstractMessage::bodyFldDsp_t(), QList(), QByteArray(), QVariant(), Envelope(), QSharedPointer( new BasicMessage("attachment", QByteArray(), AbstractMessage::bodyFldParam_t(), QByteArray(), QByteArray(), QByteArray(), 0, QByteArray(), AbstractMessage::bodyFldDsp_t(), QList(), QByteArray(), QVariant()) ), 0 )); bodyFldParam.clear(); - bodyFldParam.clear(); bodyFldParam["BOUNDARY"] = "----=_NextPart_000_0077_01CBB179.57530990"; bodyFldDsp = AbstractMessage::bodyFldDsp_t(); fetchData["BODYSTRUCTURE"] = QSharedPointer( new MultiMessage( msgList, "mixed", bodyFldParam, bodyFldDsp, QList(), QByteArray(), QVariant())); QTest::newRow("fetch-envelope-blupix-gmail") << QByteArray("* 6116 FETCH (UID 8803 RFC822.SIZE 56144 ENVELOPE (\"Tue, 11 Jan 2011 10:21:42 +0100\" " "\"blablabla\" ((\"somebody\" NIL \"info\" \"example.com\")) " "((\"somebody\" NIL \"info\" \"example.com\")) " "((\"somebody\" NIL \"info\" \"example.com\")) " "((\"destination\" NIL \"foobar\" \"gmail.com\")) " "NIL NIL NIL \"\") " "BODYSTRUCTURE (((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"iso-8859-2\") NIL NIL " "\"QUOTED-PRINTABLE\" 52 2 NIL NIL NIL)(\"TEXT\" \"HTML\" (\"CHARSET\" \"iso-8859-2\") " "NIL NIL \"QUOTED-PRINTABLE\" 1739 66 NIL NIL NIL) \"ALTERNATIVE\" " "(\"BOUNDARY\" \"----=_NextPart_001_0078_01CBB179.57530990\") NIL NIL)" "(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 836 NIL (\"ATTACHMENT\" NIL) NIL)" "(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"7BIT\" 50785 NIL (\"ATTACHMENT\" NIL) NIL) " "\"MIXED\" (\"BOUNDARY\" \"----=_NextPart_000_0077_01CBB179.57530990\") NIL NIL))\r\n") << QSharedPointer( new Fetch( 6116, fetchData ) ); + msgList.clear(); + bodyFldParam.clear(); + bodyFldParam["CHARSET"] = "UTF-8"; + msgList << QSharedPointer(new TextMessage("text", "plain", bodyFldParam, {}, {}, "QUOTED-PRINTABLE", 388, {}, {}, {}, {}, {}, 12)); + msgList << QSharedPointer(new TextMessage("text", "html", bodyFldParam, {}, {}, "QUOTED-PRINTABLE", 1576, {}, {}, {}, {}, {}, 46)); + bodyFldParam.clear(); + bodyFldParam["BOUNDARY"] = "001a113fbc1264f3cd0551bfbcf8"; + msgList = QList>() << QSharedPointer( + new MultiMessage(msgList, "alternative", bodyFldParam, {}, {}, {}, {})); + bodyFldParam.clear(); + bodyFldParam["NAME"] = "icon.png"; + bodyFldDsp = AbstractMessage::bodyFldDsp_t(); + bodyFldDsp.first = "ATTACHMENT"; + bodyFldDsp.second["FILENAME"] = "icon.png"; + msgList << QSharedPointer(new BasicMessage("image", "png", bodyFldParam, {}, {}, "BASE64", 7864, {}, bodyFldDsp, {}, {}, {})); + bodyFldParam.clear(); + bodyFldParam["BOUNDARY"] = "001a113fbc1264f3bd0551bfbcf7"; + msgList = QList>() << QSharedPointer( + new MultiMessage(msgList, "related", bodyFldParam, {}, {}, {}, {})); + msgList << QSharedPointer(new BasicMessage("message", "delivery-status", {}, {}, {}, "7BIT", 571, {}, {}, {}, {}, {})); + bodyFldParam.clear(); + bodyFldParam["x-trojita-original-mime-type"] = "message/rfc822"; + msgList << QSharedPointer( + new BasicMessage("application", "x-trojita-malformed-part-from-imap-response", bodyFldParam, {}, {}, + "QUOTED-PRINTABLE", 6109, {}, {}, {}, {}, {})); + bodyFldParam.clear(); + bodyFldParam["BOUNDARY"] = "001a113fbc1264e9640551bfbcf6"; + bodyFldParam["REPORT-TYPE"] = "delivery-status"; + fetchData.clear(); + fetchData["BODYSTRUCTURE"] = QSharedPointer( + new MultiMessage(msgList, "report", bodyFldParam, {}, {}, {}, {})); + QTest::newRow("2018-gmail-message-rfc822-not-list") + << QByteArray("* 380 FETCH (BODYSTRUCTURE (" + // A first part of multipart/report: a human-readable version. So far so good. + "(" + "(" + "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"UTF-8\") NIL NIL \"QUOTED-PRINTABLE\" 388 12 NIL NIL NIL)" + "(\"TEXT\" \"HTML\" (\"CHARSET\" \"UTF-8\") NIL NIL \"QUOTED-PRINTABLE\" 1576 46 NIL NIL NIL)" + " \"ALTERNATIVE\" (\"BOUNDARY\" \"001a113fbc1264f3cd0551bfbcf8\") NIL NIL" + ")" + "(\"IMAGE\" \"PNG\" (\"NAME\" \"icon.png\") \"\" NIL \"BASE64\" 7864 NIL (\"ATTACHMENT\" (\"FILENAME\" \"icon.png\")) NIL)" + " \"RELATED\" (\"BOUNDARY\" \"001a113fbc1264f3bd0551bfbcf7\") NIL NIL" + ")" + // The second part: a machine-readable report. OK. + "(\"MESSAGE\" \"DELIVERY-STATUS\" NIL NIL NIL \"7BIT\" 571 NIL NIL NIL)" + // Third part: this has a bug, it says that it's a message/rfc822, but it is not because it has a NIL envelope and a NIL body. + "(\"MESSAGE\" \"RFC822\" NIL NIL NIL \"QUOTED-PRINTABLE\" 6109 NIL NIL NIL)" + // the top-level multipart/report + " \"REPORT\" (\"BOUNDARY\" \"001a113fbc1264e9640551bfbcf6\" \"REPORT-TYPE\" \"delivery-status\") NIL NIL))\r\n") + << QSharedPointer(new Fetch(380, fetchData)); + fetchData.clear(); fetchData["UID"] = QSharedPointer(new RespData(42463)); fetchData["FLAGS"] = QSharedPointer(new RespData(QStringList() << QStringLiteral("\\Seen"))); fetchData["MODSEQ"] = QSharedPointer(new RespData(quint64(45278))); QTest::newRow("fetch-flags-modseq") << QByteArray("* 11235 FETCH (UID 42463 MODSEQ (45278) FLAGS (\\Seen))\r\n") << QSharedPointer(new Fetch(11235, fetchData)); fetchData.clear(); fetchData["FLAGS"] = QSharedPointer( new RespData(QStringList() << QStringLiteral("\\Seen") << QStringLiteral("\\Answered"))); QTest::newRow("fetch-two-flags") << QByteArray("* 2 FETCH (FLAGS (\\Seen \\Answered))\r\n") << QSharedPointer(new Fetch(2, fetchData)); fetchData.clear(); fetchData["UID"] = QSharedPointer(new RespData(81)); fetchData["BODY[HEADER.FIELDS (MESSAGE-ID IN-REPLY-TO REFERENCES DATE)]"] = QSharedPointer( new RespData("01234567\r\n")); QTest::newRow("fetch-header-fields-1") << QByteArray("* 81 FETCH (UID 81 BODY[HEADER.FIELDS (MESSAGE-ID In-REPLY-TO REFERENCES DATE)] {10}\r\n01234567\r\n)\r\n") << QSharedPointer(new Fetch(81, fetchData)); fetchData.clear(); fetchData["UID"] = QSharedPointer(new RespData(81)); fetchData["BODY[HEADER.FIELDS (MESSAGE-ID)]"] = QSharedPointer( new RespData("01234567\r\n")); QTest::newRow("fetch-header-fields-2") << QByteArray("* 81 FETCH (UID 81 BODY[HEADER.FIELDS (MESSAgE-Id)]{10}\r\n01234567\r\n)\r\n") << QSharedPointer(new Fetch(81, fetchData)); QTest::newRow("id-nil") << QByteArray("* ID nIl\r\n") << QSharedPointer(new Id(QMap())); QTest::newRow("id-empty") << QByteArray("* ID ()\r\n") << QSharedPointer(new Id(QMap())); QMap sampleId; sampleId["foo "] = "bar"; QTest::newRow("id-something") << QByteArray("* ID (\"foo \" \"bar\")\r\n") << QSharedPointer(new Id(sampleId)); sampleId.clear(); sampleId["v1"] = "333"; sampleId["v2"] = "666x333"; QTest::newRow("id-numeric-bits") << QByteArray("* ID (\"v1\" 333 \"v2\" 666x333)\r\n") << QSharedPointer(new Id(sampleId)); QTest::newRow("enabled-empty") << QByteArray("* ENABLED\r\n") << QSharedPointer(new Enabled(QList())); QTest::newRow("enabled-empty-space") << QByteArray("* ENABLED \r\n") << QSharedPointer(new Enabled(QList())); QTest::newRow("enabled-condstore") << QByteArray("* ENABLED CONDSToRE\r\n") << QSharedPointer(new Enabled(QList() << "CONDSToRE")); QTest::newRow("enabled-condstore-qresync") << QByteArray("* ENABLED CONDSToRE qresync\r\n") << QSharedPointer(new Enabled(QList() << "CONDSToRE" << "qresync")); QTest::newRow("vanished-one") << QByteArray("* VANIShED 1\r\n") << QSharedPointer(new Vanished(Vanished::NOT_EARLIER, Imap::Uids() << 1)); QTest::newRow("vanished-earlier-one") << QByteArray("* VANIShED (EARlIER) 1\r\n") << QSharedPointer(new Vanished(Vanished::EARLIER, Imap::Uids() << 1)); QTest::newRow("vanished-earlier-set") << QByteArray("* VANISHED (EARLIER) 300:303,405,411\r\n") << QSharedPointer(new Vanished(Vanished::EARLIER, Imap::Uids() << 300 << 301 << 302 << 303 << 405 << 411)); QTest::newRow("genurlauth-1") << QByteArray("* GENURLAUTH \"imap://joe@example.com/INBOX/;uid=20/;section=1.2;urlauth=submit+fred:internal:91354a473744909de610943775f92038\"\r\n") << QSharedPointer(new GenUrlAuth(QStringLiteral("imap://joe@example.com/INBOX/;uid=20/;section=1.2;urlauth=submit+fred:internal:91354a473744909de610943775f92038"))); QTest::newRow("genurlauth-2") << QByteArray("* GENURLAUTH meh\r\n") << QSharedPointer(new GenUrlAuth(QStringLiteral("meh"))); QSharedPointer modSeqData(new RespData(5875136264581852368ULL)); QTest::newRow("highestmodseq-64bit") << QByteArray("* OK [HIGHESTMODSEQ 5875136264581852368] x\r\n") << QSharedPointer(new State("", OK, QStringLiteral("x"), HIGHESTMODSEQ, modSeqData)); fetchData.clear(); fetchData["UID"] = QSharedPointer(new RespData(123)); fetchData["MODSEQ"] = QSharedPointer(new RespData(5875136264581852368ULL)); QTest::newRow("fetch-highestmodseq-64bit") << QByteArray("* 33 FETCH (UID 123 MODSEQ (5875136264581852368))\r\n") << QSharedPointer(new Fetch(33, fetchData)); fetchData.clear(); from.clear(); sender = replyTo = to = cc = bcc = from; fetchData["ENVELOPE"] = QSharedPointer(new RespData( Envelope(QDateTime(), QByteArray(), from, sender, replyTo, to, cc, bcc, QList(), QByteArray("CAPunWhCDfYrqx_Px-072QAjZbog2DFz9O=48WnCxaxOov-VNVQ@mail.gmail.com")))); QTest::newRow("aox-messageid-spacing") << QByteArray("* 666 FETCH (ENVELOPE (NIL NIL NIL NIL NIL NIL NIL NIL NIL {70}\r\n" "\n ))\r\n") << QSharedPointer(new Fetch(666, fetchData)); } /** @short Test that parsing this garbage doesn't result in an expceiton In a erfect world with unlimited resources, it would be nice to verify the result of the parsing. However, I'm trying to feed real-world data to the test cases and this might mean having to construct tens of lines using code which is just tedious to create. This is better than nothing. */ void ImapParserParseTest::testParseFetchGarbageWithoutExceptions() { QFETCH(QByteArray, line); try { QSharedPointer r = parser->parseUntagged(line); QVERIFY(r.dynamicCast()); } catch (const Imap::ParseError &err) { qDebug() << err.what(); QFAIL("Parsing resulted in an exception"); } } void ImapParserParseTest::testParseFetchGarbageWithoutExceptions_data() { QTest::addColumn("line"); QTest::newRow("fetch-seznam.cz-quirk-minus-two-as-uint") << QByteArray("* 997 FETCH (UID 8636 ENVELOPE (NIL \"Merkantilismus\" ((\"\" NIL \"tony.szturc\" \"seznam.cz\")) " "((\"\" NIL \"tony.szturc\" \"seznam.cz\")) ((\"\" NIL \"tony.szturc\" \"seznam.cz\")) " "((\"\" NIL \"xxx\" \"seznam.cz\")) NIL NIL NIL \"\") INTERNALDATE \"15-Jan-2013 12:17:06 +0000\" " "BODYSTRUCTURE ((\"text\" \"plain\" (\"charset\" \"us- ascii\") NIL NIL \"7bit\" -2 1 NIL NIL NIL)" "(\"application\" \"msword\" (\"name\" " "\"=?utf-8?B?NC10cmFkaWNuaV9ob3Nwb2RhcnNrZV9teXNsZW5pLG5vdm92ZWtlX2Vrb25vbWlja2VfdGVvcmllLmRvY2==?=\") " "NIL NIL \"base64\" 51146 NIL NIL NIL)" "(\"application\" \"msword\" (\"name\" \"=?utf-8?B?NS1ob3Nwb2RhcnNreXZ5dm9qLmRvY2==?=\") NIL NIL " "\"base64\" 205984 NIL NIL NIL)" "(\"application\" \"vnd.ms-powerpoint\" (\"name\" \"=?utf-8?B?Mi5faG9zcG9kYXJza3lfdnl2b2pfY2Vza3ljaF96ZW1pLnBwdH==?=\") " "NIL NIL \"base64\" 3986596 NIL NIL NIL) \"mixed\" NIL NIL NIL) RFC822.SIZE 4245505)\r\n"); // Test data from http://thread.gmane.org/gmane.mail.squirrelmail.user/26433 QTest::newRow("random-squirrelmail-report") << QByteArray("* 1 FETCH (BODYSTRUCTURE " "(((\"text\" \"plain\" (\"charset\" \"US-ASCII\") NIL NIL \"7bit\" 2 1 NIL NIL " "NIL)(\"text\" \"html\" (\"charset\" \"US-ASCII\") NIL NIL \"quoted-printable\" 486 " "10 NIL NIL NIL) \"alternative\" (\"boundary\" " "\"-----------------------------1131387468\") NIL NIL)(\"message\" \"rfc822\" NIL " "NIL NIL NIL 191761 (\"Thu, 3 Nov 2005 14:19:49 EST\" \"Fwd: Fw: Fw:\" ((NIL " "NIL \"Ernestgerp12\" \"aol.com\")) NIL NIL ((NIL NIL \"BagLady1933\" " "\"aol.com\")(NIL NIL \"BOBPEGRUKS\" \"cs.com\")(NIL NIL \"bren804\" " "\"webtv.net\")(NIL NIL \"khgsk\" \"comcast.net\")(NIL NIL \"KOKOGOSIK\" " "\"COMCAST.NET\")(NIL NIL \"MAG2215\" \"aol.com\")(NIL NIL \"Mamabenc\" " "\"aol.com\")(NIL NIL \"Ifix2th\" \"aol.com\")(NIL NIL \"Ooosal\" \"aol.com\")(NIL " "NIL \"rtalalaj\" \"comcast.net\")) NIL NIL NIL \"e3.1fdeccbc.309bbcd5 aol.com\") " "(((\"text\" \"plain\" (\"charset\" \"US-ASCII\") NIL NIL \"7bit\" 2 1 NIL NIL " "NIL)(\"text\" \"html\" (\"charset\" \"US-ASCII\") NIL NIL \"quoted-printable\" 486 " "10 NIL NIL NIL) \"alternative\" (\"boundary\" " "\"-----------------------------1131045588\") NIL NIL)(\"message\" \"rfc822\" NIL " "NIL NIL NIL 190153 NIL (((\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL NIL " "\"quoted-printable\" 6584 293 NIL NIL NIL)(\"text\" \"html\" (\"charset\" " "\"iso-8859-1\") NIL NIL \"quoted-printable\" 61190 948 NIL NIL NIL) " "\"alternative\" (\"boundary\" \"----=_NextPart_001_004C_01C5E044.F95592A0\") NIL " "NIL)(\"image\" \"gif\" (\"name\" \"ATT0000712221112121.gif\") NIL NIL \"base64\" " "68496 NIL NIL NIL)(\"image\" \"gif\" (\"name\" \"ATT0001023332223232.gif\") NIL " "NIL \"base64\" 20038 NIL NIL NIL)(\"image\" \"gif\" (\"name\" " "\"image001334343.gif\") NIL NIL \"base64\" 10834 NIL NIL NIL)(\"image\" \"gif\" " "(\"name\" \"ATT0001646665545455.gif\") NIL NIL \"base64\" 7146 NIL NIL " "NIL)(\"audio\" \"mid\" (\"name\" \"ATT0001951116651516.mid\") NIL NIL \"base64\" " "12938 NIL NIL NIL) \"related\" (\"boundary\" " "\"----=_NextPart_000_004B_01C5E044.F95592A0\" \"type\" " "\"multipart/alternative\") NIL NIL) NIL NIL (\"INLINE\" NIL) NIL) \"mixed\" " "(\"boundary\" \"part2_d7.319e0427.309bbcd5_boundary\") NIL NIL) NIL NIL " "(\"INLINE\" NIL) NIL) \"mixed\" (\"boundary\" " "\"part1_d7.319e0427.30a0f44c_boundary\") NIL NIL)" " )\r\n"); QTest::newRow("davmail-by-ashp") << QByteArray("* 1125 FETCH (BODYSTRUCTURE ((\"TEXT\" \"HTML\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 562 7)" "(\"APPLICATION\" \"OCTET-STREAM\" (\"NAME\" \"zzz.xml\") NIL " "\"ZZZ.XML\" \"BASE64\" NIL NIL) \"MIXED\"))\r\n"); } void ImapParserParseTest::benchmark() { QByteArray line1 = "* 1 FETCH (BODYSTRUCTURE ((\"text\" \"plain\" " "(\"charset\" \"US-ASCII\" \"delsp\" \"yes\" \"format\" \"flowed\") " "NIL NIL \"7bit\" 990 27 NIL NIL NIL)" "(\"application\" \"pgp-signature\" (\"x-mac-type\" \"70674453\" \"name\" \"PGP.sig\") NIL " "\"This is a digitally signed message part\" \"7bit\" 193 NIL (\"inline\" " "(\"filename\" \"PGP.sig\")) NIL) \"signed\" (\"protocol\" " "\"application/pgp-signature\" \"micalg\" \"pgp-sha1\" \"boundary\" " "\"Apple-Mail-10--856231115\") NIL NIL))\r\n"; QByteArray line2 = "* 13 FETCH (ENVELOPE (NIL " "\"IMAP4rev1 WG mtg summary and minutes\" " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((\"Terry Gray\" NIL \"gray\" \"cac.washington.edu\")) " "((NIL NIL \"imap\" \"cac.washington.edu\")) " "((NIL NIL \"minutes\" \"CNRI.Reston.VA.US\") " "(\"John Klensin\" NIL \"KLENSIN\" \"MIT.EDU\")) NIL NIL " "\"\"))\r\n"; QByteArray line3 = "* 123 FETCH (InternalDate \"6-Apr-1981 12:03:32 -0630\")\r\n"; QBENCHMARK { parser->processLine( line1 ); parser->processLine( line2 ); parser->processLine( line3 ); while (parser->hasResponse()) parser->getResponse(); } } void ImapParserParseTest::benchmarkInitialChat() { QByteArray line4 = "* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE STARTTLS AUTH=PLAIN] Dovecot ready.\r\n"; QByteArray line5 = "* CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE STARTTLS AUTH=PLAIN\r\n"; QByteArray line6 = "1 OK Pre-login capabilities listed, post-login capabilities have more.\r\n"; QByteArray line7 = "* CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY " "THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 " "CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS SPECIAL-USE\r\n"; QByteArray line8 = "2 OK Logged in\r\n"; QByteArray line9 = "3 OK Capability completed.\r\n"; QByteArray line10 = "* ENABLED QRESYNC\r\n"; QByteArray line11 = "4 OK Enabled.\r\n"; QByteArray line12 = "* NAMESPACE ((\"\" \".\")) NIL NIL\r\n"; QByteArray line13 = "5 OK Namespace completed.\r\n"; QBENCHMARK { parser->processLine(line4); parser->processLine(line5); parser->processLine(line6); parser->processLine(line7); parser->processLine(line8); parser->processLine(line9); parser->processLine(line10); parser->processLine(line11); parser->processLine(line12); parser->processLine(line13); while (parser->hasResponse()) parser->getResponse(); } } void ImapParserParseTest::testSequences() { QFETCH( Imap::Sequence, sequence ); QFETCH( QByteArray, muster ); QCOMPARE( sequence.toByteArray(), muster ); } void ImapParserParseTest::testSequences_data() { QTest::addColumn("sequence"); QTest::addColumn("muster"); QTest::newRow("sequence-one") << Imap::Sequence( 33 ) << QByteArray("33"); QTest::newRow("sequence-unlimited") << Imap::Sequence::startingAt(666) << QByteArray("666:*"); QTest::newRow("sequence-range") << Imap::Sequence( 333, 666 ) << QByteArray("333:666"); QTest::newRow("sequence-distinct") << Imap::Sequence( 20 ).add( 10 ).add( 30 ) << QByteArray("10,20,30"); QTest::newRow("sequence-collapsed-2") << Imap::Sequence( 10 ).add( 11 ) << QByteArray("10:11"); QTest::newRow("sequence-collapsed-3") << Imap::Sequence( 10 ).add( 11 ).add( 12 ) << QByteArray("10:12"); QTest::newRow("sequence-head-and-collapsed") << Imap::Sequence( 3 ).add( 31 ).add( 32 ).add( 33 ) << QByteArray("3,31:33"); QTest::newRow("sequence-collapsed-and-tail") << Imap::Sequence( 666 ).add( 31 ).add( 32 ).add( 33 ) << QByteArray("31:33,666"); QTest::newRow("sequence-head-collapsed-tail") << Imap::Sequence( 666 ).add( 31 ).add( 32 ).add( 1 ).add( 33 ) << QByteArray("1,31:33,666"); QTest::newRow("sequence-same") << Imap::Sequence( 2 ).add( 2 ) << QByteArray("2"); QTest::newRow("sequence-multiple-consequent") << Imap::Sequence( 2 ).add( 3 ).add( 4 ).add( 6 ).add( 7 ) << QByteArray("2:4,6:7"); QTest::newRow("sequence-complex") << Imap::Sequence( 2 ).add( 3 ).add( 4 ).add( 6 ).add( 7 ).add( 1 ) .add( 100 ).add( 101 ).add( 102 ).add( 99 ).add( 666 ).add( 333 ).add( 666 ) << QByteArray("1:4,6:7,99:102,333,666"); QTest::newRow("sequence-from-list-1") << Imap::Sequence::fromVector(Imap::Uids() << 2 << 3 << 4 << 6 << 7 << 1 << 100 << 101 << 102 << 99 << 666 << 333 << 666) << QByteArray("1:4,6:7,99:102,333,666"); } /** @short Test responses which fail to parse */ void ImapParserParseTest::testThrow() { QFETCH(QByteArray, line); QFETCH(QString, exceptionClass); QFETCH(QString, error); try { QSharedPointer ptr = parser->parseUntagged(line); QVERIFY2(!ptr, "should have thrown"); } catch (Imap::ImapException &e) { QCOMPARE(QString::fromUtf8(e.exceptionClass().c_str()), exceptionClass); QCOMPARE(QString::fromUtf8(e.msg().c_str()), error); } } /** @short Test data for testThrow() */ void ImapParserParseTest::testThrow_data() { QTest::addColumn("line"); QTest::addColumn("exceptionClass"); QTest::addColumn("error"); QTest::newRow("garbage") << QByteArray("blah") << QStringLiteral("UnrecognizedResponseKind") << QStringLiteral("AH"); // uppercase of "blah" after cutting the two leading characters QTest::newRow("garbage-well-formed") << QByteArray("* blah\r\n") << QStringLiteral("UnrecognizedResponseKind") << QStringLiteral("BLAH"); QTest::newRow("expunge-number-at-the-end") << QByteArray("* expunge 666\r\n") << QStringLiteral("UnexpectedHere") << QStringLiteral("Malformed response: the number should go first"); QTest::newRow("thread-non-numbers") << QByteArray("* THREAD (ahoj)\r\n") << QStringLiteral("UnexpectedHere") << QStringLiteral("THREAD response: cannot parse \"ahoj\" as an unsigned integer"); } QTEST_GUILESS_MAIN( ImapParserParseTest ) namespace QTest { template<> char * toString( const Imap::Responses::AbstractResponse& resp ) { QByteArray buf; QTextStream stream( &buf ); stream << resp; stream.flush(); return qstrdup( buf.data() ); } }