/**************************************************************************** ** ** Copyright (C) 2015 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the Qt Messaging Framework. ** ** $QT_BEGIN_LICENSE:LGPL21$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see http://www.qt.io/terms-conditions. For further ** information use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 or version 3 as published by the Free ** Software Foundation and appearing in the file LICENSE.LGPLv21 and ** LICENSE.LGPLv3 included in the packaging of this file. Please review the ** following information to ensure the GNU Lesser General Public License ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** As a special exception, The Qt Company gives you certain additional ** rights. These rights are described in The Qt Company LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "imapstructure.h" #include #include namespace { template void processChars(Processor p, const QString& field, QString::const_iterator origin) { bool quoted = false; bool escaped = false; int depth = 0; QString::const_iterator begin = origin, it = begin, end = field.end(); do { char c((*it).toLatin1()); switch (c) { case ')': if (!quoted && !escaped) { if (it != begin) { if (p(c, depth, quoted, field, begin, it)) { begin = it + 1; } } --depth; } break; case '(': if (!quoted && !escaped) { if (p(c, depth, quoted, field, begin, it)) { begin = it + 1; } ++depth; } break; case '"': if (p(c, depth, quoted, field, begin, it)) { begin = it + 1; } if (!escaped) { quoted = !quoted; } break; default: if (p(c, depth, quoted, field, begin, it)) { begin = it + 1; } break; } escaped = (c == '\\'); } while (++it != end); if ((it != begin) && (depth >= 0)) { // Terminate the sequence p('\0', depth, quoted, field, begin, it); } } struct StructureDecomposer { int reportDepth; QStringList result; StructureDecomposer() : reportDepth(0) {} bool operator()(char c, int depth, bool, const QString &field, QString::const_iterator begin, QString::const_iterator it) { if ((c == ')') || (c == '\0')) { if (depth == reportDepth) { // Allow reportDepth to have negative values, meaning that we're // processing more fields than requested (happens with Yahoo IMAP) // When processing a negative depth, the unrequested content is silently // discarded if (depth >= 0) result.append(field.mid(begin - field.begin(), it - begin).trimmed()); --reportDepth; return true; } } else if (c == '(') { if (it == begin) { if (reportDepth != 0) return false; ++reportDepth; return true; } } return false; } }; QStringList decomposeStructure(const QString &structure, int offset) { if (structure.isEmpty()) return QStringList(); StructureDecomposer sd; processChars(sd, structure, structure.begin() + offset); return sd.result; } struct ElementDecomposer { int reportDepth; QStringList result; ElementDecomposer() : reportDepth(0) {} void append(const QString &token, int begin, int length, int depth) { if ((depth == 0) && (!token.isEmpty()) && (token.at(begin) == QChar('"')) && (token.at(begin + length - 1) == QChar('"'))) { ++begin; length -= 2; } result.append(token.mid(begin, length)); } bool operator()(char c, int depth, bool quoted, const QString &field, QString::const_iterator begin, QString::const_iterator it) { if (((c == ' ') && !quoted) || (c == '\0')) { if (depth == 0) { if (it != begin) { append(field, begin - field.begin(), it - begin, depth); } return true; } } else if (c == ')') { if (depth == reportDepth) { append(field, begin - field.begin(), it - begin, depth); if (reportDepth > 0) --reportDepth; return true; } } else if (c == '(') { if (it == begin) { if (reportDepth > 0) return false; ++reportDepth; return true; } } return false; } }; QStringList decomposeElements(const QString &element) { if (element.isEmpty() || (element.trimmed().toUpper() == "NIL")) return QStringList(); ElementDecomposer ed; processChars(ed, element, element.begin()); return ed.result; } QMailMessageBody::TransferEncoding fromEncodingDescription(const QString &desc) { if (!desc.isEmpty()) { const QString value(desc.trimmed().toUpper()); // TODO: these strings aren't actually specified by RFC3501... if (value == "7BIT") { return QMailMessageBody::SevenBit; } else if (value == "8BIT") { return QMailMessageBody::EightBit; } else if (value == "BINARY") { return QMailMessageBody::Binary; } else if (value == "QUOTED-PRINTABLE") { return QMailMessageBody::QuotedPrintable; } else if (value == "BASE64") { return QMailMessageBody::Base64; } } return QMailMessageBody::NoEncoding; } QMailMessagePartContainer::MultipartType fromMultipartDescription(const QString &desc) { if (!desc.isEmpty()) { const QString value(desc.trimmed().toUpper()); // TODO: these strings aren't actually specified by RFC3501... if (value == "MIXED") { return QMailMessagePartContainer::MultipartMixed; } else if (value == "ALTERNATIVE") { return QMailMessagePartContainer::MultipartAlternative; } else if (value == "DIGEST") { return QMailMessagePartContainer::MultipartDigest; } else if (value == "SIGNED") { return QMailMessagePartContainer::MultipartSigned; } else if (value == "ENCRYPTED") { return QMailMessagePartContainer::MultipartEncrypted; } else if (value == "PARALLEL") { return QMailMessagePartContainer::MultipartParallel; } else if (value == "RELATED") { return QMailMessagePartContainer::MultipartRelated; } else if (value == "FORMDATA") { return QMailMessagePartContainer::MultipartFormData; } else if (value == "REPORT") { return QMailMessagePartContainer::MultipartReport; } } return QMailMessagePartContainer::MultipartNone; } QMailMessageContentDisposition fromDispositionDescription(const QString &desc, const QString &size) { QMailMessageContentDisposition disposition; QStringList details(decomposeElements(desc)); if (!details.isEmpty()) { const QString value(details.at(0).trimmed().toUpper()); if (value == "INLINE") { disposition.setType(QMailMessageContentDisposition::Inline); } else { // Anything else should be treated as attachment disposition.setType(QMailMessageContentDisposition::Attachment); } if (details.count() > 1) { const QStringList parameters(decomposeElements(details.at(1))); if (parameters.count() % 2) qWarning() << "Incorrect fromDispositionDescription parameters:" << parameters; QStringList::const_iterator it = parameters.begin(), end = parameters.end(); for ( ; (it != end) && (it + 1 != end); ++it) { disposition.setParameter((*it).toLatin1(), (*(it + 1)).toLatin1()); ++it; } } } else { // Default to inline for no specification disposition.setType(QMailMessageContentDisposition::Inline); } if (!size.isEmpty() && disposition.size() == -1) { // We use the body size only if Content-Disposition/Size is not available. // From RFC3501 INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1: // body size // A number giving the size of the body in octets. Note that // this size is the size in its transfer encoding and not the // resulting size after any decoding. disposition.setSize(size.toInt()); } return disposition; } void setBodyFromDescription(const QStringList &details, QMailMessagePartContainer *container, uint *size, bool *wellFormed) { QMailMessageContentType type; // [0]: type // [1]: sub-type type.setType(details.at(0).toLatin1()); type.setSubType(details.at(1).toLatin1()); // [2]: parameter list const QStringList parameters(decomposeElements(details.at(2))); if (parameters.count() % 2) { qWarning() << "Incorrect setBodyFromDescription parameters:" << parameters; if (wellFormed) { *wellFormed = false; } } QStringList::const_iterator it = parameters.begin(), end = parameters.end(); for ( ; (it != end) && ((it + 1) != end); it += 2) { type.setParameter((*it).toLatin1(), (*(it + 1)).toLatin1()); } // [3]: content-ID // [4]: content-description // [5]: content-encoding QMailMessageBody::TransferEncoding encoding(fromEncodingDescription(details.at(5))); // [6]: size if (size) { bool ok; *size = details.at(6).toUInt(&ok); if (!ok && wellFormed) { *wellFormed = false; } } container->setBody(QMailMessageBody::fromData(QByteArray(), type, encoding, QMailMessageBody::AlreadyEncoded)); } void setPartContentFromStructure(const QStringList &structure, QMailMessagePart *part, uint *size, bool *wellFormed); void setPartFromDescription(const QStringList &details, QMailMessagePart *part) { // [3]: content-ID const QString &id(details.at(3)); if (!id.isEmpty() && (id.trimmed().toUpper() != "NIL")) { part->setContentID(id); } // [4]: content-description const QString &description(details.at(4)); if (!description.isEmpty() && (description.trimmed().toUpper() != "NIL")) { part->setContentDescription(description); } // [6]: content size const QString &size(details.at(6)); int next = 7; const QMailMessageContentType &type(part->contentType()); if (type.matches("text")) { // The following field is the part's line count ++next; } else if (type.matches("message", "rfc822")) { // For parts of type 'message/rfc822', there are three extra fields here... next += 3; } // [6 + n + 1]: MD5-sum of content // Ignore the potential MD5-sum for now ++next; // [6 + n + 2]: content-disposition QString disposition; if (next < details.count()) { disposition = details.at(next); if (disposition.trimmed().toUpper() == "NIL") { disposition.clear(); } } part->setContentDisposition(fromDispositionDescription(disposition, size)); ++next; // [6 + n + 3]: content-language if (next < details.count()) { const QString &language(details.at(next)); // TODO: this may need compression from multiple tokens to one? if (!language.isEmpty() && (language.trimmed().toUpper() != "NIL")) part->setContentLanguage(language); } ++next; // [6 + n + 3]: content-location if (next < details.count()) { const QString &location(details.at(next)); // TODO: this may need compression from multiple tokens to one? if (!location.isEmpty() && (location.trimmed().toUpper() != "NIL")) part->setContentLocation(location); } ++next; } void setMultipartFromDescription(const QStringList &structure, QMailMessagePartContainer *container, QMailMessagePart *part, uint *size, bool *wellFormed) { QStringList details = decomposeElements(structure.last()); // [0]: type container->setMultipartType(fromMultipartDescription(details.at(0))); // [1]: parameter list if (details.count() > 1) { const QStringList parameters(decomposeElements(details.at(1))); QStringList::const_iterator it = parameters.begin(), end = parameters.end(); if (parameters.count() % 2) { qWarning() << "Incorrect setMultipartFromDescription parameter count" << parameters.last(); if (wellFormed) { *wellFormed = false; } } for ( ; (it != end) && ((it + 1) != end); it += 2) { if ((*it).trimmed().toUpper() == "BOUNDARY") { const QString boundary((*(it + 1))); if (boundary.toUpper() == "NIL") { container->setBoundary(QByteArray()); } else { container->setBoundary(boundary.toLatin1()); } } } } if (part) { // [2]: content-disposition QString disposition; if (details.count() > 2) { disposition = details.at(2); if (disposition.trimmed().toUpper() == "NIL") { disposition.clear(); } } part->setContentDisposition(fromDispositionDescription(disposition, QString())); // [3]: content-language if (details.count() > 3) { const QString &language(details.at(3)); // TODO: this may need compression from multiple tokens to one? if (!language.isEmpty() && (language.trimmed().toUpper() != "NIL")) part->setContentLanguage(language); } // [4]: content-location if (details.count() > 4) { const QString &location(details.at(4)); // TODO: this may need compression from multiple tokens to one? if (!location.isEmpty() && (location.trimmed().toUpper() != "NIL")) part->setContentLocation(location); } } // Create the other pieces described by the structure for (int i = 0; i < (structure.count() - 1); ++i) { QMailMessagePart part; uint partSize = 0; setPartContentFromStructure(decomposeStructure(structure.at(i), 0), &part, &partSize, wellFormed); container->appendPart(part); if (size) { *size += partSize; } } } void setPartContentFromStructure(const QStringList &structure, QMailMessagePart *part, uint *size, bool *wellFormed) { if (!structure.isEmpty()) { // The last element is the message const QString &message = structure.last(); if (!message.isEmpty()) { if (structure.count() == 1) { QStringList details(decomposeElements(message)); if (details.count() < 7) { qWarning() << "Ill-formed part structure:" << details; if (wellFormed) { *wellFormed = false; } } else { setBodyFromDescription(details, part, size, wellFormed); if (details.count() > 7) { setPartFromDescription(details, part); } } } else { // This is a multi-part message setMultipartFromDescription(structure, part, part, size, wellFormed); } } } } } bool setMessageContentFromStructure(const QStringList &structure, QMailMessage *message) { bool wellFormed = false; if (!structure.isEmpty()) { wellFormed = true; // The last element is the message description const QString &description = structure.last(); if (!description.isEmpty()) { uint size = 0; if (structure.count() == 1) { QStringList details(decomposeElements(description)); if (details.count() < 7) { qWarning() << "Ill-formed body structure:" << details; wellFormed = false; } else { setBodyFromDescription(details, message, &size, &wellFormed); } } else { // This is a multi-part message setMultipartFromDescription(structure, message, 0, &size, &wellFormed); } message->setContentSize(size); if (!wellFormed) { // Ill-formed message set status of message as not retrieved message->setStatus(QMailMessage::ContentAvailable, false); message->setStatus(QMailMessage::PartialContentAvailable, false); } } // See if any of the parts are attachments if (message->hasAttachments()) { message->setStatus( QMailMessage::HasAttachments, true ); } if (message->isEncrypted()) { message->setStatus(QMailMessage::HasEncryption, true); } } return wellFormed; } QStringList getMessageStructure(const QString &field) { static const QString marker("BODYSTRUCTURE ("); int index = field.indexOf(marker, Qt::CaseInsensitive); if (index != -1) { return decomposeStructure(field, index + marker.length()); } return QStringList(); }