/**************************************************************************** ** ** 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 "browserwidget.h" #include #include #include #include "qmaillog.h" #include #include #include #include #include #include #include #ifdef USE_WEBKIT #include #include #include #include #include #include #else #include #include #endif #include static QColor replyColor(Qt::darkGreen); static QString dateString(const QDateTime& dt) { QDateTime current = QDateTime::currentDateTime(); if (dt.date() == current.date()) { //today return QString(qApp->translate("Browser", "Today %1")).arg(dt.toString("h:mm:ss ap")); } else if (dt.daysTo(current) == 1) { //yesterday return QString(qApp->translate("Browser", "Yesterday %1")).arg(dt.toString("h:mm:ss ap")); } else if (dt.daysTo(current) < 7) { //within 7 days return dt.toString("dddd h:mm:ss ap"); } else { return dt.toString("dd/MM/yy h:mm:ss ap"); } } #ifdef USE_WEBKIT class ContentReply : public QNetworkReply { Q_OBJECT public: ContentReply(QObject *parent, QByteArray *data, const QString &contentType); ~ContentReply(); qint64 bytesAvailable() const; qint64 readData(char *data, qint64 maxSize); void abort(); private: QBuffer m_buffer; }; ContentReply::ContentReply(QObject *parent, QByteArray *data, const QString &contentType) : QNetworkReply(parent) { setOpenMode(QIODevice::ReadOnly | QIODevice::Unbuffered); setHeader(QNetworkRequest::ContentTypeHeader, contentType); m_buffer.setBuffer(data); m_buffer.open(QIODevice::ReadOnly); QTimer::singleShot(0, this, SIGNAL(readyRead())); QTimer::singleShot(0, this, SIGNAL(finished())); } ContentReply::~ContentReply() { } qint64 ContentReply::bytesAvailable() const { return m_buffer.bytesAvailable() + QIODevice::bytesAvailable(); } qint64 ContentReply::readData(char *data, qint64 maxSize) { return m_buffer.read(data, maxSize); } void ContentReply::abort() { m_buffer.close(); } class ContentAccessManager : public QNetworkAccessManager { Q_OBJECT public: ContentAccessManager(QObject *parent); ~ContentAccessManager(); void setResource(const QSet &identifiers, const QByteArray &data, const QString &contentType); void clear(); protected: QNetworkReply *createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData = Q_NULLPTR); private: QMap > m_data; }; ContentAccessManager::ContentAccessManager(QObject *parent) : QNetworkAccessManager(parent) { } ContentAccessManager::~ContentAccessManager() { } void ContentAccessManager::setResource(const QSet &identifiers, const QByteArray &data, const QString &contentType) { foreach (const QUrl url, identifiers) { m_data.insert(url, qMakePair(data, contentType)); } } void ContentAccessManager::clear() { m_data.clear(); } QNetworkReply *ContentAccessManager::createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData) { if (op == QNetworkAccessManager::GetOperation) { QUrl url(http://code.qt.io/req.url()); if ((url.scheme() == "cid") || (url.scheme() == "qmf-part")) { QString identifier(url.path().trimmed()); // Remove any delimiters that are present if (identifier.startsWith('<') && identifier.endsWith('>')) { identifier = identifier.mid(1, identifier.length() - 2); } // See if we have any data for this content identifier QMap >::iterator it = m_data.find(QUrl("cid:" + identifier)); if (it == m_data.end()) { it = m_data.find(QUrl("qmf-part:" + identifier)); } if (it != m_data.end()) { return new ContentReply(this, &it.value().first, it.value().second); } } } return QNetworkAccessManager::createRequest(op, req, outgoingData); } #else class ContentRenderer : public QTextBrowser { Q_OBJECT public: ContentRenderer(QWidget *parent); ~ContentRenderer(); void setResource(const QUrl &name, const QVariant &var); virtual QVariant loadResource(int type, const QUrl &name); void clear(); private: QMap m_data; }; ContentRenderer::ContentRenderer(QWidget *parent) : QTextBrowser(parent) { } ContentRenderer::~ContentRenderer() { } void ContentRenderer::setResource(const QUrl& name, const QVariant &var) { if (!m_data.contains(name)) { m_data.insert(name, var); } } void ContentRenderer::clear() { m_data.clear(); } QVariant ContentRenderer::loadResource(int type, const QUrl& name) { if (m_data.contains(name)) { return m_data[name]; } return QTextBrowser::loadResource(type, name); } #endif BrowserWidget::BrowserWidget( QWidget *parent ) : QWidget( parent ), replySplitter( &BrowserWidget::handleReplies ) { QLayout* l = new QVBoxLayout(this); l->setSpacing(0); l->setContentsMargins(0,0,0,0); #ifdef USE_WEBKIT m_accessManager = new ContentAccessManager(this); m_webView = new QWebView(this); m_webView->setObjectName("renderer"); m_webView->page()->setNetworkAccessManager(m_accessManager); m_webView->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_webView,SIGNAL(linkClicked(QUrl)),this,SIGNAL(anchorClicked(QUrl))); connect(m_webView,SIGNAL(customContextMenuRequested(QPoint)),this,SLOT(contextMenuRequested(QPoint))); l->addWidget(m_webView); #else m_renderer = new ContentRenderer(this); m_renderer->setObjectName("renderer"); m_renderer->setFrameStyle(QFrame::NoFrame); m_renderer->setContextMenuPolicy(Qt::CustomContextMenu); m_renderer->setOpenLinks(false); connect(m_renderer,SIGNAL(anchorClicked(QUrl)),this,SIGNAL(anchorClicked(QUrl))); connect(m_renderer,SIGNAL(customContextMenuRequested(QPoint)),this,SLOT(contextMenuRequested(QPoint))); l->addWidget(m_renderer); #endif setFocusPolicy( Qt::StrongFocus ); } #ifndef USE_WEBKIT void BrowserWidget::setResource( const QUrl& name, QVariant var ) { m_renderer->setResource(name, var); } #endif void BrowserWidget::clearResources() { #ifdef USE_WEBKIT m_accessManager->clear(); #else m_renderer->clear(); #endif numbers.clear(); } QList BrowserWidget::embeddedNumbers() const { QList result; return result; } void BrowserWidget::setTextResource(const QSet& names, const QString& textData, const QString &contentType) { #ifdef USE_WEBKIT m_accessManager->setResource(names, textData.toUtf8(), contentType); #else QVariant data(textData); foreach (const QUrl &url, names) { setResource(url, data); } Q_UNUSED(contentType); #endif } void BrowserWidget::setImageResource(const QSet& names, const QByteArray& imageData, const QString &contentType) { // Create a image from the data QDataStream imageStream(&const_cast(imageData), QIODevice::ReadOnly); QImageReader imageReader(imageStream.device()); // Max size should be bounded by our display window, which will possibly // have a vertical scrollbar (and a small fudge factor...) int maxWidth = (width() - style()->pixelMetric(QStyle::PM_ScrollBarExtent) - 4); QSize imageSize; if (imageReader.supportsOption(QImageIOHandler::Size)) { imageSize = imageReader.size(); // See if the image needs to be down-scaled during load if (imageSize.width() > maxWidth) { // And the loaded size should maintain the image aspect ratio imageSize.scale(maxWidth, (INT_MAX >> 4), Qt::KeepAspectRatio); imageReader.setQuality( 49 ); // Otherwise Qt smooth scales imageReader.setScaledSize(imageSize); #ifdef USE_WEBKIT } else { // We can use the image directly m_accessManager->setResource(names, imageData, contentType); return; #endif } } QImage image = imageReader.read(); if (!imageReader.supportsOption(QImageIOHandler::Size)) { // We need to scale it down now if (image.width() > maxWidth) { image = image.scaled(maxWidth, INT_MAX, Qt::KeepAspectRatio); #ifdef USE_WEBKIT } else { // We can use the image directly m_accessManager->setResource(names, imageData, contentType); return; #endif } } #ifdef USE_WEBKIT QByteArray scaledData; { // Write the scaled image data to a buffer QBuffer buffer(&scaledData); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); // default format doesn't render in webkit } m_accessManager->setResource(names, scaledData, contentType); #else QVariant data(image); foreach (const QUrl &url, names) { setResource(url, data); } Q_UNUSED(contentType); #endif } void BrowserWidget::setPartResource(const QMailMessagePart& part) { QSet names; #ifdef USE_WEBKIT QString name(part.displayName()); if (!name.isEmpty()) { // use 'qmf-part' url scheme to ensure inline images without a contentId are rendered names.insert(QUrl("qmf-part:" + name.toHtmlEscaped())); } name = part.contentID().toHtmlEscaped(); if (!name.isEmpty()) { // We can only resolve URLs using the cid: scheme if (name.startsWith("cid:", Qt::CaseInsensitive)) { names.insert(QUrl(name)); } else { names.insert(QUrl("cid:" + name)); } } #else QString name(part.displayName()); if (!name.isEmpty()) { names.insert(QUrl(name.toHtmlEscaped())); } name = part.contentID().toHtmlEscaped(); if (!name.isEmpty()) { // Add the content both with and without the cid: prefix names.insert(name); if (name.startsWith("cid:", Qt::CaseInsensitive)) { names.insert(QUrl(name.mid(4))); } else { names.insert(QUrl("cid:" + name)); } } name = part.contentType().name(); if (!name.isEmpty()) { names.insert(QUrl(name.toHtmlEscaped())); } #endif const QMailMessageContentType &ct(part.contentType()); // Ignore any charset info in the content type, since we extract string data as unicode QString contentType(ct.type() + '/' + ct.subType()); if (ct.type().toLower() == "text") { setTextResource(names, part.body().data(), contentType); } else if (ct.type().toLower() == "image") { setImageResource(names, part.body().data(QMailMessageBody::Decoded), contentType); } } void BrowserWidget::setSource(const QUrl &name) { Q_UNUSED(name) // We deal with this ourselves. // QTextBrowser::setSource( name ); } void BrowserWidget::contextMenuRequested(const QPoint& pos) { #ifdef USE_WEBKIT QMenu menu(this); menu.addAction(m_webView->pageAction(QWebPage::Copy)); menu.addAction(m_webView->pageAction(QWebPage::CopyLinkToClipboard)); menu.addSeparator(); menu.addActions(m_webView->actions()); menu.exec(mapToGlobal(pos)); #else QMenu* menu = m_renderer->createStandardContextMenu(); menu->addSeparator(); menu->addActions(m_renderer->actions()); menu->exec(mapToGlobal(pos)); delete menu; #endif } void BrowserWidget::setMessage(const QMailMessage& email, bool plainTextMode) { if (plainTextMode) { // Complete MMS messages must be displayed in HTML if (email.messageType() == QMailMessage::Mms) { QString mmsType = email.headerFieldText("X-Mms-Message-Type"); if (mmsType.contains("m-retrieve-conf") || mmsType.contains("m-send-req")) plainTextMode = false; } } // Maintain original linelengths if display width allows it if (email.messageType() == QMailMessage::Sms) { replySplitter = &BrowserWidget::smsBreakReplies; } else { uint lineCharLength; if ( fontInfo().pointSize() >= 10 ) { lineCharLength = width() / (fontInfo().pointSize() - 4 ); } else { lineCharLength = width() / (fontInfo().pointSize() - 3 ); } // Determine how to split lines in text if ( lineCharLength >= 78 ) replySplitter = &BrowserWidget::noBreakReplies; else replySplitter = &BrowserWidget::handleReplies; } if (plainTextMode) displayPlainText(&email); else displayHtml(&email); } void BrowserWidget::displayPlainText(const QMailMessage* mail) { QString bodyText; if ( (mail->status() & QMailMessage::Incoming) && !(mail->status() & QMailMessage::PartialContentAvailable) ) { if ( !(mail->status() & QMailMessage::Removed) ) { bodyText += '\n' + tr("Awaiting download") + '\n'; bodyText += tr("Size of message") + ": " + describeMailSize(mail->size()); } else { // TODO: what? } } else { if (mail->hasBody()) { bodyText += mail->body().data(); } else { if ( mail->multipartType() == QMailMessagePartContainer::MultipartAlternative ) { const QMailMessagePart* bestPart = 0; // Find the best alternative for text rendering for ( uint i = 0; i < mail->partCount(); i++ ) { const QMailMessagePart &part = mail->partAt( i ); // TODO: A good implementation would be able to extract the plain text parts // from text/html and text/enriched... if (part.contentType().type().toLower() == "text") { if (part.contentType().subType().toLower() == "plain") { // This is the best part for us bestPart = ∂ break; } else if (part.contentType().subType().toLower() == "html") { // This is the worst, but still acceptable, text part for us if (bestPart == 0) bestPart = ∂ } else { // Some other text - better than html, probably if ((bestPart != 0) && (bestPart->contentType().subType().toLower() == "html")) bestPart = ∂ } } } if (bestPart != 0) bodyText += bestPart->body().data() + '\n'; else bodyText += QLatin1String("\n<") + tr("Message part is not displayable") + QLatin1String(">\n"); } else if ( mail->multipartType() == QMailMessagePartContainer::MultipartRelated ) { const QMailMessagePart* startPart = &mail->partAt(0); // If not specified, the first part is the start QByteArray startCID = mail->contentType().parameter("start"); if (!startCID.isEmpty()) { for ( uint i = 1; i < mail->partCount(); i++ ) if (mail->partAt(i).contentID() == startCID) { startPart = &mail->partAt(i); break; } } // Render the start part, if possible if (startPart->contentType().type().toLower() == "text") bodyText += startPart->body().data() + '\n'; else bodyText += QLatin1String("\n<") + tr("Message part is not displayable") + QLatin1String(">\n"); } else { // According to RFC 2046, any unrecognised type should be treated as 'mixed' if (mail->multipartType() != QMailMessagePartContainer::MultipartMixed) qWarning() << "Generic viewer: Unimplemented multipart type:" << mail->contentType().toString(); // Render each succesive part to text, where possible for ( uint i = 0; i < mail->partCount(); i++ ) { const QMailMessagePart &part = mail->partAt( i ); if (part.hasBody() && (part.contentType().type().toLower() == "text")) { bodyText += part.body().data() + '\n'; } else { bodyText += QLatin1String("\n<") + tr("Part") + ": " + part.displayName() + QLatin1String(">\n"); } } } } } QString text; if ((mail->messageType() != QMailMessage::Sms) && (mail->messageType() != QMailMessage::Instant)) text += tr("Subject") + QLatin1String(": ") + mail->subject().simplified() + '\n'; QMailAddress fromAddress(mail->from()); if (!fromAddress.isNull()) text += tr("From") + ": " + fromAddress.toString() + '\n'; if (mail->to().count() > 0) { text += tr("To") + ": "; text += QMailAddress::toStringList(mail->to()).join(QLatin1String(", ")); } if (mail->cc().count() > 0) { text += '\n' + tr("CC") + QLatin1String(": "); text += QMailAddress::toStringList(mail->cc()).join(QLatin1String(", ")); } if (mail->bcc().count() > 0) { text += '\n' + tr("BCC") + QLatin1String(": "); text += QMailAddress::toStringList(mail->bcc()).join(QLatin1String(", ")); } if ( !mail->replyTo().isNull() ) { text += '\n' + tr("Reply-To") + QLatin1String(": "); text += mail->replyTo().toString(); } text += '\n' + tr("Date") + QLatin1String(": "); text += dateString(mail->date().toLocalTime()) + '\n'; if (mail->status() & QMailMessage::Removed) { if (!bodyText.isEmpty()) { // Don't include the notice - the most likely reason to view plain text // is for printing, and we don't want to print the notice } else { text += '\n'; text += tr("Message deleted from server"); } } if (!bodyText.isEmpty()) { text += '\n'; text += bodyText; } setPlainText(text); } static QString replaceLast(const QString container, const QString& before, const QString& after) { QString result(container); int index; if ((index = container.lastIndexOf(before)) != -1) result.replace(index, before.length(), after); return result; } QString BrowserWidget::renderSimplePart(const QMailMessagePart& part) { QString result; QString partId = part.displayName().toHtmlEscaped(); QMailMessageContentType contentType = part.contentType(); if ( contentType.type().toLower() == "text") { // No tr if (part.hasBody()) { QString partText = part.body().data(); if ( !partText.isEmpty() ) { if ( contentType.subType().toLower() == "html" ) { result = partText + "
"; } else { result = formatText( partText ); } } } else { result = renderAttachment(part); } } else if ( contentType.type().toLower() == "image") { // No tr setPartResource(part); #ifdef USE_WEBKIT result = ""; #else result = ""; #endif } else { result = renderAttachment(part); } return result; } QString BrowserWidget::renderAttachment(const QMailMessagePart& part) { QString partId = part.displayName().toHtmlEscaped(); QString attachmentTemplate = "
ATTACHMENT_TEXT: NAME_TEXTDISPOSITION
"; attachmentTemplate = replaceLast(attachmentTemplate, "ATTACHMENT_TEXT", tr("Attachment")); attachmentTemplate = replaceLast(attachmentTemplate, "ATTACHMENT_ACTION", part.partialContentAvailable() ? "view" : "retrieve"); attachmentTemplate = replaceLast(attachmentTemplate, "ATTACHMENT_LOCATION", part.location().toString(false)); attachmentTemplate = replaceLast(attachmentTemplate, "NAME_TEXT", partId); return replaceLast(attachmentTemplate, "DISPOSITION", part.partialContentAvailable() ? "" : tr(" (on server)")); } QString BrowserWidget::renderPart(const QMailMessagePart& part) { QString result; if (part.multipartType() != QMailMessage::MultipartNone) { result = renderMultipart(part); } else { bool displayAsAttachment(!part.contentAvailable()); if (!displayAsAttachment) { QMailMessageContentDisposition disposition = part.contentDisposition(); if (!disposition.isNull() && disposition.type() == QMailMessageContentDisposition::Attachment) { displayAsAttachment = true; } } result = (displayAsAttachment ? renderAttachment(part) : renderSimplePart(part)); } return result; } QString BrowserWidget::renderMultipart(const QMailMessagePartContainer& partContainer) { QString result; if (partContainer.multipartType() == QMailMessagePartContainer::MultipartAlternative) { int partIndex = -1; // Find the best alternative for rendering for (uint i = 0; i < partContainer.partCount(); ++i) { const QMailMessagePart &part = partContainer.partAt(i); // Parts are ordered simplest to most complex QString type(part.contentType().type().toLower()); if ((type == "text") || (type == "image")) { // These parts are probably displayable partIndex = i; } } if (partIndex != -1) { result += renderPart(partContainer.partAt(partIndex)); } else { result += "\n<" + tr("No displayable part") + ">\n"; } } else if (partContainer.multipartType() == QMailMessagePartContainer::MultipartRelated) { uint startIndex = 0; // If not specified, the first part is the start QByteArray startCID = partContainer.contentType().parameter("start"); if (!startCID.isEmpty()) { for (uint i = 1; i < partContainer.partCount(); ++i) { if (partContainer.partAt(i).contentID() == startCID) { startIndex = i; break; } } } // Add any other parts as resources QList absentParts; for (uint i = 0; i < partContainer.partCount(); ++i) { if (i != startIndex) { const QMailMessagePart &part = partContainer.partAt(i); if (part.partialContentAvailable()) { setPartResource(partContainer.partAt(i)); } else { absentParts.append(&part); } } } // Render the start part result += renderPart(partContainer.partAt(startIndex)); // Show any unavailable parts as attachments foreach (const QMailMessagePart *part, absentParts) { result += renderAttachment(*part); } } else { // According to RFC 2046, any unrecognised type should be treated as 'mixed' if (partContainer.multipartType() != QMailMessagePartContainer::MultipartMixed) qWarning() << "Unimplemented multipart type:" << partContainer.contentType().toString(); // Render each part successively according to its disposition for (uint i = 0; i < partContainer.partCount(); ++i) { result += renderPart(partContainer.partAt(i)); } } return result; } typedef QPair TextPair; void BrowserWidget::displayHtml(const QMailMessage* mail) { QString subjectText, bodyText; QList metadata; // For SMS messages subject is the same as body, so for SMS don't // show the message text twice (same for IMs) if ((mail->messageType() != QMailMessage::Sms) && (mail->messageType() != QMailMessage::Instant)) subjectText = mail->subject(); QString from = mail->headerFieldText("From"); if (!from.isEmpty() && from != "\"\" <>") // ugh metadata.append(qMakePair(tr("From"), refMailTo( mail->from() ))); if (mail->to().count() > 0) metadata.append(qMakePair(tr("To"), listRefMailTo( mail->to() ))); if (mail->cc().count() > 0) metadata.append(qMakePair(tr("CC"), listRefMailTo( mail->cc() ))); if (mail->bcc().count() > 0) metadata.append(qMakePair(tr("BCC"), listRefMailTo( mail->bcc() ))); if (!mail->replyTo().isNull()) metadata.append(qMakePair(tr("Reply-To"), refMailTo( mail->replyTo() ))); metadata.append(qMakePair(tr("Date"), dateString(mail->date().toLocalTime()))); metadata.append(qMakePair(tr("Message Id"), QString::number(mail->id().toULongLong()))); metadata.append(qMakePair(tr("CalendarInvitation"), (mail->status() & QMailMessage::CalendarInvitation) ? QString("true") : QString("false"))); metadata.append(qMakePair(tr("CalendarCancellation"), (mail->status() & QMailMessage::CalendarCancellation) ? QString("true") : QString("false"))); if ( (mail->status() & QMailMessage::Incoming) && !(mail->status() & QMailMessage::PartialContentAvailable) ) { if ( !(mail->status() & QMailMessage::Removed) ) { bodyText = "WAITING_TEXT
" "SIZE_TEXT
" "
" "DOWNLOAD_TEXT"; bodyText = replaceLast(bodyText, "WAITING_TEXT", tr("Awaiting download")); bodyText = replaceLast(bodyText, "SIZE_TEXT", tr("Size of message content") + ": " + describeMailSize(mail->contentSize())); bodyText = replaceLast(bodyText, "DOWNLOAD_TEXT", tr("Download this message")); } else { // TODO: what? } } else { if (mail->partCount() > 0) { bodyText = renderMultipart(*mail); } else { bodyText = (mail->content() == QMailMessage::PlainTextContent) ? formatText(mail->body().data()) : mail->body().data(); if (!mail->contentAvailable()) { QString trailer = "
" "WAITING_TEXT
" "SIZE_TEXT
" "DOWNLOAD_TEXT"; trailer = replaceLast(trailer, "WAITING_TEXT", tr("More data available")); trailer = replaceLast(trailer, "SIZE_TEXT", tr("Size") + ": " + describeMailSize(mail->body().length()) + tr(" of ") + describeMailSize(mail->contentSize())); if ((mail->contentType().type().toLower() == "text") && (mail->contentType().subType().toLower() == "plain")) { trailer = replaceLast(trailer, "DOWNLOAD_ACTION", "download;5120"); } else { trailer = replaceLast(trailer, "DOWNLOAD_ACTION", "download"); } trailer = replaceLast(trailer, "DOWNLOAD_TEXT", tr("Retrieve more data")); bodyText += trailer; } } } // Form our parts into a displayable page QString pageData; if (mail->status() & QMailMessage::Removed) { QString noticeTemplate = "
" "NOTICE_TEXT
" "
"; QString notice = tr("Message deleted from server"); if (!bodyText.isEmpty()) { notice.prepend("["); notice.append("]"); } pageData += replaceLast(noticeTemplate, "NOTICE_TEXT", notice); } QString headerTemplate = \ "
" "" "" "" "" "
" "" "" "" "" "" "" "" "
" "SUBJECT_TEXT" "
" "" "METADATA_TEXT" "
" "
" "
" "
" "
"; headerTemplate = replaceLast(headerTemplate, "HIGHLIGHT_COLOR", palette().color(QPalette::Highlight).name()); headerTemplate = replaceLast(headerTemplate, "LINK_COLOR", palette().color(QPalette::HighlightedText).name()); headerTemplate = replaceLast(headerTemplate, "SUBJECT_TEXT", subjectText.toHtmlEscaped()); headerTemplate = replaceLast(headerTemplate, "WINDOW_COLOR", palette().color(QPalette::Window).name()); QString itemTemplate = "" "" "ID_TEXT: " "" "" "CONTENT_TEXT" "" ""; QString metadataText; foreach (const TextPair item, metadata) { QString element = replaceLast(itemTemplate, "ID_TEXT", item.first.toHtmlEscaped()); element = replaceLast(element, "CONTENT_TEXT", item.second); metadataText.append(element); } pageData += replaceLast(headerTemplate, "METADATA_TEXT", metadataText); QString bodyTemplate = "
BODY_TEXT
"; pageData += replaceLast(bodyTemplate, "BODY_TEXT", bodyText); QString pageTemplate = "" "" "" "" "
" "PAGE_DATA" "
"; QString html = replaceLast(pageTemplate,"PAGE_DATA",pageData); #ifdef USE_WEBKIT m_webView->setHtml(html); m_webView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); #else m_renderer->setHtml(html); #endif } QString BrowserWidget::describeMailSize(uint bytes) const { QString size; // No translation? if (bytes < 1024) { size.setNum(bytes); size += " Bytes"; } else if (bytes < 1024*1024) { size.setNum( (bytes / 1024) ); size += " KB"; } else { float f = static_cast( bytes )/ (1024*1024); size.setNum(f, 'g', 3); size += " MB"; } return size; } QString BrowserWidget::formatText(const QString& txt) const { return (*this.*replySplitter)(txt); } QString BrowserWidget::smsBreakReplies(const QString& txt) const { /* Preserve white space, add linebreaks so text is wrapped to fit the display width */ QString str = ""; QStringList p = txt.split('\n'); QStringList::Iterator it = p.begin(); while ( it != p.end() ) { str += buildParagraph( *it, "", true ) + "
"; it++; } return str; } QString BrowserWidget::noBreakReplies(const QString& txt) const { /* Maintains the original linebreaks, but colours the lines according to reply level */ QString str = ""; QStringList p = txt.split('\n'); int x, levelList; QStringList::Iterator it = p.begin(); while ( it != p.end() ) { x = 0; levelList = 0; while (x < (*it).length() ) { if ( (*it)[x] == '>' ) { levelList++; } else if ( (*it)[x] == ' ' ) { } else break; x++; } if (levelList == 0 ) { str += encodeUrlAndMail(*it) + "
"; } else { const QString para("%2
"); str += para.arg(replyColor.name()).arg(encodeUrlAndMail(*it)); } it++; } while ( str.endsWith("
") ) { str.chop(4); //remove trailing br } return str; } QString appendLine(const QString& preceding, const QString& suffix) { if (suffix.isEmpty()) return preceding; QString result(preceding); int nwsIndex = suffix.indexOf(QRegularExpression("[^\\s]")); if (nwsIndex > 0) { // This line starts with whitespace, which we'll have to protect to keep // We can't afford to make huge tracts of whitespace; ASCII art will be broken! // Convert any run of up to 4 spaces to a tab; convert all tabs to two spaces each QString leader(suffix.left(nwsIndex)); leader.replace(QRegularExpression(" {1,4}"), "\t"); // Convert the spaces to non-breaking leader.replace("\t", "  "); result.append(leader).append(suffix.mid(nwsIndex)); } else result.append(suffix); return result; } QString unwrap(const QString& txt, const QString& prepend) { QStringList lines = txt.split('\n', Qt::KeepEmptyParts); QString result; result.reserve(txt.length()); QStringList::iterator it = lines.begin(), prev = it, end = lines.end(); if (it != end) { for (++it; it != end; ++prev, ++it) { QString terminator = "
"; int prevLength = (*prev).length(); if (prevLength == 0) { // If the very first line is empty, skip it; otherwise include if (prev == lines.begin()) continue; } else { int wsIndex = (*it).indexOf(QRegularExpression("\\s")); if (wsIndex == 0) { // This was probably an intentional newline } else { if (wsIndex == -1) wsIndex = (*it).length(); bool logicalEnd = false; const QChar last = (*prev)[prevLength - 1]; logicalEnd = ((last == '.') || (last == '!') || (last == '?')); if ((*it)[0].isUpper() && logicalEnd) { // This was probably an intentional newline } else { int totalLength = prevLength + prepend.length(); if ((wsIndex != -1) && ((totalLength + wsIndex) > 78)) { // This was probably a forced newline - convert it to a space terminator = ' '; } } } } result = appendLine(result, BrowserWidget::encodeUrlAndMail(*prev) + terminator); } if (!(*prev).isEmpty()) result = appendLine(result, BrowserWidget::encodeUrlAndMail(*prev)); } return result; } /* This one is a bit complicated. It divides up all lines according to their reply level, defined as count of ">" before normal text It then strips them from the text, builds the formatted paragraph and inserts them back into the beginning of each line. Probably not too speed efficient on large texts, but this manipulation greatly increases the readability (trust me, I'm using this program for my daily email reading..) */ QString BrowserWidget::handleReplies(const QString& txt) const { QStringList out; QStringList p = txt.split('\n'); QList levelList; QStringList::Iterator it = p.begin(); uint lastLevel = 0, level = 0; // Skip the last string, if it's non-existent int offset = (txt.endsWith('\n') ? 1 : 0); QString str, line; while ( (it + offset) != p.end() ) { line = (*it); level = 0; if ( line.startsWith(">") ) { for (int x = 0; x < line.length(); x++) { if ( line[x] == ' ') { // do nothing } else if ( line[x] == '>' ) { level++; if ( (level > 1 ) && (line[x-1] != ' ') ) { line.insert(x, ' '); //we need it to be "> > " etc.. x++; } } else { // make sure it follows style "> > This is easier to format" if ( line[x - 1] != ' ' ) line.insert(x, ' '); break; } } } if ( level != lastLevel ) { if ( !str.isEmpty() ) { out.append( str ); levelList.append( lastLevel ); } str.clear(); lastLevel = level; it--; } else { str += line.mid(level * 2) + '\n'; } it++; } if ( !str.isEmpty() ) { out.append( str ); levelList.append( level ); } str = ""; lastLevel = 0; int pos = 0; it = out.begin(); while ( it != out.end() ) { if ( levelList[pos] == 0 ) { str += unwrap( *it, "" ) + "
"; } else { QString pre = ""; QString preString = ""; for ( uint x = 0; x < levelList[pos]; x++) { pre += "> "; preString += "> "; } QString segment = unwrap( *it, preString ); const QString para("%2
"); str += para.arg(replyColor.name()).arg(pre + segment); } lastLevel = levelList[pos]; pos++; it++; } while ( str.endsWith("
") ) { str.chop(4); //remove trailing br } return str; } QString BrowserWidget::buildParagraph(const QString& txt, const QString& prepend, bool preserveWs) const { Q_UNUSED(prepend); QStringList out; QString input = encodeUrlAndMail( preserveWs ? txt : txt.simplified() ); if (preserveWs) return input.replace('\n', "
"); QStringList p = input.split( ' ', Qt::SkipEmptyParts ); return p.join(QString(' ')); } QString BrowserWidget::encodeUrlAndMail(const QString& txt) { QStringList result; // TODO: is this necessary? // Find and encode URLs that aren't already inside anchors //QRegExp anchorPattern("<\\s*a\\s*href.*/\\s*a\\s*>"); //anchorPattern.setMinimal(true); // We should be optimistic in our URL matching - the link resolution can // always fail, but if we don't match it, then we can't make it into a link QRegularExpression urlPattern("(" "(?:http|https|ftp)://" "|" "mailto:" ")?" // optional scheme "((?:[^: \\t]+:)?[^ \\t@]+@)?" // optional credentials "(" // either: "localhost" // 'localhost' "|" // or: "(?:" // one-or-more: "[A-Za-z\\d]" // one: legal char, "(?:" // zero-or-one: "[A-Za-z\\d-]*[A-Za-z\\d]" // (zero-or-more: (legal char or '-'), one: legal char) ")?" // end of optional group "\\." // '.' ")+" // end of mandatory group "[A-Za-z\\d]" // one: legal char "(?:" // zero-or-one: "[A-Za-z\\d-]*[A-Za-z\\d]" // (zero-or-more: (legal char or '-'), one: legal char) ")?" // end of optional group ")" // end of alternation "(:\\d+)?" // optional port number "(" // optional location "/" // beginning with a slash "[A-Za-z\\d\\.\\!\\#\\$\\%\\'" // containing any sequence of legal chars "\\*\\+\\-\\/\\=\\?\\^\\_\\`" "\\{\\|\\}\\~\\&\\(\\)]*" ")?"); QRegularExpressionMatch urlMatch; // Find and encode file:// links QRegularExpression filePattern("(file://\\S+)"); QRegularExpressionMatch fileMatch; // Find and encode email addresses QRegularExpression addressPattern(QMailAddress::emailAddressPattern()); QRegularExpressionMatch addressMatch; int urlPos = txt.indexOf(urlPattern, 0, &urlMatch); int addressPos = txt.indexOf(addressPattern, 0, &fileMatch); int filePos = txt.indexOf(filePattern, 0, &addressMatch); int lastPos = 0, matchLength = 0; while ((urlPos != -1) || (addressPos != -1) || (filePos != -1)) { int *matchPos = 0; QRegularExpression *matchPattern = 0; // Which pattern has the first match? if ((urlPos != -1) && ((addressPos == -1) || (addressPos >= urlPos)) && ((filePos == -1) || (filePos >= urlPos))) { matchPos = &urlPos; matchPattern = &urlPattern; matchLength = urlMatch.capturedLength(0); } else if ((addressPos != -1) && ((urlPos == -1) || (urlPos >= addressPos)) && ((filePos == -1) || (filePos >= addressPos))) { matchPos = &addressPos; matchPattern = &addressPattern; matchLength = addressMatch.capturedLength(0); } else if ((filePos != -1) && ((urlPos == -1) || (urlPos >= filePos)) && ((addressPos == -1) || (addressPos >= filePos))) { matchPos = &filePos; matchPattern = &filePattern; matchLength = fileMatch.capturedLength(0); } else { Q_ASSERT(false); return QString(); } QString replacement; if (matchPattern == &urlPattern) { // Is this a valid URL? QString scheme = urlMatch.captured(1); QString credentials = urlMatch.captured(2); QString host(urlMatch.captured(3)); // Ensure that the host is not purely a number // Also ignore credentials with no scheme if (scheme.isEmpty() && ((host.indexOf(QRegularExpression("[^\\d\\.]")) == -1) || (!credentials.isEmpty()))) { // Ignore this match urlPos = txt.indexOf(urlPattern, urlPos + 1); continue; } else { char parenTypes[] = { '(', ')', '[', ']', '{', '}', '<', '>', '\0' }; QString leading; QString trailing; QString url = urlMatch.captured(0); QChar firstChar(url.at(0)); QChar lastChar(url.at(url.length() - 1)); for (int i = 0; parenTypes[i] != '\0'; i += 2) { if ((firstChar == parenTypes[i]) || (lastChar == parenTypes[i+1])) { // Check for unbalanced parentheses int left = url.count(parenTypes[i]); int right = url.count(parenTypes[i+1]); if ((right > left) && (lastChar == parenTypes[i+1])) { // The last parenthesis is probably not part of the URL url = url.left(url.length() - 1); trailing.append(lastChar); lastChar = url.at(url.length() - 1); } else if ((right < left) && (firstChar == parenTypes[i])) { // The first parenthesis is probably not part of the URL url = url.mid(1); leading.append(firstChar); firstChar = url.at(0); } } } if ((lastChar == '.') || (lastChar == ',') || (lastChar == ';')) { // The last character is probably part of the surrounding text url = url.left(url.length() - 1); trailing = lastChar; } replacement = refUrl(url, scheme, leading, trailing); } } else if (matchPattern == &addressPattern) { QString address = addressMatch.captured(0); replacement = refMailTo(QMailAddress(address)); } else if (matchPattern == &filePattern) { QString file = fileMatch.captured(0); replacement = refUrl(file, "file://", QString(), QString()); } // Write the unmatched text out in escaped form result.append(txt.mid(lastPos, (*matchPos - lastPos)).toHtmlEscaped()); result.append(replacement); // Find the following pattern match for this pattern lastPos = *matchPos + matchLength; *matchPos = txt.indexOf(*matchPattern, lastPos); // Bypass any other matches contained within the matched text if ((urlPos != -1) && (urlPos < lastPos)) { urlPos = txt.indexOf(urlPattern, lastPos); } if ((addressPos != -1) && (addressPos < lastPos)) { addressPos = txt.indexOf(addressPattern, lastPos); } if ((filePos != -1) && (filePos < lastPos)) { filePos = txt.indexOf(filePattern, lastPos); } } if (lastPos < txt.length()) { result.append(txt.mid(lastPos).toHtmlEscaped()); } return result.join(""); } void BrowserWidget::scrollToAnchor(const QString& anchor) { #ifdef USE_WEBKIT // TODO Q_UNUSED(anchor); #else m_renderer->scrollToAnchor(anchor); #endif } void BrowserWidget::setPlainText(const QString& text) { #ifdef USE_WEBKIT QString html(text.toHtmlEscaped()); html.replace("\n", "
"); m_webView->setHtml("" + html + ""); m_webView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); #else m_renderer->setPlainText(text); #endif } void BrowserWidget::addAction(QAction* action) { #ifdef USE_WEBKIT m_webView->addAction(action); #else m_renderer->addAction(action); #endif } void BrowserWidget::addActions(const QList& actions) { #ifdef USE_WEBKIT m_webView->addActions(actions); #else m_renderer->addActions(actions); #endif } void BrowserWidget::removeAction(QAction* action) { #ifdef USE_WEBKIT m_webView->removeAction(action); #else m_renderer->removeAction(action); #endif } QString BrowserWidget::listRefMailTo(const QList& list) { QStringList result; foreach ( const QMailAddress& address, list ) result.append( refMailTo( address ) ); return result.join( ", " ); } QString BrowserWidget::refMailTo(const QMailAddress& address) { QString name = address.toString().toHtmlEscaped(); if (name == "System") return name; if (address.isPhoneNumber() || address.isEmailAddress()) return "" + name + ""; return name; } QString BrowserWidget::refNumber(const QString& number) { return "" + number + ""; } QString BrowserWidget::refUrl(const QString& url, const QString& scheme, const QString& leading, const QString& trailing) { // Assume HTTP if there is no scheme QString escaped(url.toHtmlEscaped()); QString target(scheme.isEmpty() ? "http://" + escaped : escaped); return leading.toHtmlEscaped() + "" + escaped + "" + trailing.toHtmlEscaped(); } #include "browserwidget.moc"