/* Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Copyright (c) 2010 Leo Franchi Copyright (c) 2016 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mailtemplates.h" #include #include #include #include #include #include #include #include #include #include #include "stringhtmlwriter.h" #include "objecttreesource.h" #include "csshelper.h" #include namespace KMime { namespace Types { static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right) { return (left.asString() == right.asString()); } static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right) { return (left.addrSpec().asString() == right.addrSpec().asString()); } } } static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KMime::Types::AddrSpecList me) { KMime::Types::Mailbox::List addresses(list); for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) { if (me.contains(it->addrSpec())) { it = addresses.erase(it); } else { ++it; } } return addresses; } void initHeader(const KMime::Message::Ptr &message) { message->removeHeader(); message->removeHeader(); message->date()->setDateTime(QDateTime::currentDateTime()); const QStringList extraInfo = QStringList() << QSysInfo::prettyProductName(); message->userAgent()->fromUnicodeString(QString("%1/%2(%3)").arg(QString::fromLocal8Bit("Kube")).arg("0.1").arg(extraInfo.join(",")), "utf-8"); // This will allow to change Content-Type: message->contentType()->setMimeType("text/plain"); } QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, bool replace, const QString &newPrefix) { bool recognized = false; // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); QRegExp rx(bigRegExp, Qt::CaseInsensitive); if (rx.isValid()) { QString tmp = str; if (rx.indexIn(tmp) == 0) { recognized = true; if (replace) { return tmp.replace(0, rx.matchedLength(), newPrefix + QLatin1String(" ")); } } } else { qWarning() << "bigRegExp = \"" << bigRegExp << "\"\n" << "prefix regexp is invalid!"; // try good ole Re/Fwd: recognized = str.startsWith(newPrefix); } if (!recognized) { return newPrefix + QLatin1String(" ") + str; } else { return str; } } QString cleanSubject(const KMime::Message::Ptr &msg, const QStringList &prefixRegExps, bool replace, const QString &newPrefix) { return replacePrefixes(msg->subject()->asUnicodeString(), prefixRegExps, replace, newPrefix); } QString forwardSubject(const KMime::Message::Ptr &msg) { bool replaceForwardPrefix = true; QStringList forwardPrefixes; forwardPrefixes << "Fwd:"; forwardPrefixes << "FW:"; return cleanSubject(msg, forwardPrefixes, replaceForwardPrefix, QStringLiteral("Fwd:")); } QString replySubject(const KMime::Message::Ptr &msg) { bool replaceReplyPrefix = true; QStringList replyPrefixes; //We're escaping the regex escape sequences. awesome replyPrefixes << "Re\\\\s*:"; replyPrefixes << "Re[\\\\d+\\\\]:"; replyPrefixes << "Re\\\\d+:"; return cleanSubject(msg, replyPrefixes, replaceReplyPrefix, QStringLiteral("Re:")); } QByteArray getRefStr(const KMime::Message::Ptr &msg) { QByteArray firstRef, lastRef, refStr, retRefStr; int i, j; if (auto hdr = msg->references(false)) { refStr = hdr->as7BitString(false).trimmed(); } if (refStr.isEmpty()) { return msg->messageID()->as7BitString(false); } i = refStr.indexOf('<'); j = refStr.indexOf('>'); firstRef = refStr.mid(i, j - i + 1); if (!firstRef.isEmpty()) { retRefStr = firstRef + ' '; } i = refStr.lastIndexOf('<'); j = refStr.lastIndexOf('>'); lastRef = refStr.mid(i, j - i + 1); if (!lastRef.isEmpty() && lastRef != firstRef) { retRefStr += lastRef + ' '; } retRefStr += msg->messageID()->as7BitString(false); return retRefStr; } KMime::Content *createPlainPartContent(const KMime::Message::Ptr &msg, const QString &plainBody) { KMime::Content *textPart = new KMime::Content(msg.data()); textPart->contentType()->setMimeType("text/plain"); //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text // QTextCodec *charset = selectCharset(m_charsets, plainBody); // textPart->contentType()->setCharset(charset->name()); textPart->contentType()->setCharset("utf-8"); textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); textPart->fromUnicodeString(plainBody); return textPart; } KMime::Content *createMultipartAlternativeContent(const KMime::Message::Ptr &msg, const QString &plainBody, const QString &htmlBody) { KMime::Content *multipartAlternative = new KMime::Content(msg.data()); multipartAlternative->contentType()->setMimeType("multipart/alternative"); const QByteArray boundary = KMime::multiPartBoundary(); multipartAlternative->contentType()->setBoundary(boundary); KMime::Content *textPart = createPlainPartContent(msg, plainBody); multipartAlternative->addContent(textPart); KMime::Content *htmlPart = new KMime::Content(msg.data()); htmlPart->contentType()->setMimeType("text/html"); //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text // QTextCodec *charset = selectCharset(m_charsets, htmlBody); // htmlPart->contentType()->setCharset(charset->name()); textPart->contentType()->setCharset("utf-8"); htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); htmlPart->fromUnicodeString(htmlBody); multipartAlternative->addContent(htmlPart); return multipartAlternative; } void addProcessedBodyToMessage(const KMime::Message::Ptr &msg, const QString &plainBody, const QString &htmlBody, bool forward) { //FIXME // MessageCore::ImageCollector ic; // ic.collectImagesFrom(mOrigMsg.data()); // Now, delete the old content and set the new content, which // is either only the new text or the new text with some attachments. auto parts = msg->contents(); foreach (KMime::Content *content, parts) { msg->removeContent(content, true/*delete*/); } msg->contentType()->clear(); // to get rid of old boundary const QByteArray boundary = KMime::multiPartBoundary(); KMime::Content *const mainTextPart = htmlBody.isEmpty() ? createPlainPartContent(msg, plainBody) : createMultipartAlternativeContent(msg, plainBody, htmlBody); mainTextPart->assemble(); KMime::Content *textPart = mainTextPart; // if (!ic.images().empty()) { // textPart = createMultipartRelated(ic, mainTextPart); // textPart->assemble(); // } // If we have some attachments, create a multipart/mixed mail and // add the normal body as well as the attachments KMime::Content *mainPart = textPart; //FIXME // if (forward) { // auto attachments = mOrigMsg->attachments(); // attachments += mOtp->nodeHelper()->attachmentsOfExtraContents(); // if (!attachments.isEmpty()) { // mainPart = createMultipartMixed(attachments, textPart); // mainPart->assemble(); // } // } msg->setBody(mainPart->encodedBody()); msg->setHeader(mainPart->contentType()); msg->setHeader(mainPart->contentTransferEncoding()); msg->assemble(); msg->parse(); } QString plainToHtml(const QString &body) { QString str = body; str = str.toHtmlEscaped(); str.replace(QStringLiteral("\n"), QStringLiteral("
\n")); return str; } //TODO implement this function using a DOM tree parser void makeValidHtml(QString &body, const QString &headElement) { QRegExp regEx; regEx.setMinimal(true); regEx.setPattern(QStringLiteral("")); if (!body.isEmpty() && !body.contains(regEx)) { regEx.setPattern(QStringLiteral("")); if (!body.contains(regEx)) { body = QLatin1String("") + body + QLatin1String("
"); } regEx.setPattern(QStringLiteral("")); if (!body.contains(regEx)) { body = QLatin1String("") + headElement + QLatin1String("") + body; } body = QLatin1String("") + body + QLatin1String(""); } } QString stripSignature(const QString &msg) { // Following RFC 3676, only > before -- // I prefer to not delete a SB instead of delete good mail content. const QRegExp sbDelimiterSearch = QRegExp(QLatin1String("(^|\n)[> ]*-- \n")); // The regular expression to look for prefix change const QRegExp commonReplySearch = QRegExp(QLatin1String("^[ ]*>")); QString res = msg; int posDeletingStart = 1; // to start looking at 0 // While there are SB delimiters (start looking just before the deleted SB) while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) { QString prefix; // the current prefix QString line; // the line to check if is part of the SB int posNewLine = -1; // Look for the SB beginning int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart); // The prefix before "-- "$ if (res.at(posDeletingStart) == QLatin1Char('\n')) { ++posDeletingStart; } prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart); posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1; // now go to the end of the SB while (posNewLine < res.size() && posNewLine > 0) { // handle the undefined case for mid ( x , -n ) where n>1 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine); if (nextPosNewLine < 0) { nextPosNewLine = posNewLine - 1; } line = res.mid(posNewLine, nextPosNewLine - posNewLine); // check when the SB ends: // * does not starts with prefix or // * starts with prefix+(any substring of prefix) if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) || (!prefix.isEmpty() && line.startsWith(prefix) && line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) { posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1; } else { break; // end of the SB } } // remove the SB or truncate when is the last SB if (posNewLine > 0) { res.remove(posDeletingStart, posNewLine - posDeletingStart); } else { res.truncate(posDeletingStart); } } return res; } QString plainMessageText(MessageViewer::ObjectTreeParser &otp, bool aStripSignature) { QString result = otp.plainTextContent(); if (result.isEmpty()) { //HTML-only mails QWebPage doc; doc.mainFrame()->setHtml(otp.htmlContent()); result = doc.mainFrame()->toPlainText(); } if (aStripSignature) { result = stripSignature(result); } return result; } QString htmlMessageText(MessageViewer::ObjectTreeParser &otp, bool aStripSignature, QString &headElement) { QString htmlElement = otp.htmlContent(); if (htmlElement.isEmpty()) { //plain mails only QString htmlReplace = otp.plainTextContent().toHtmlEscaped(); htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("
")); htmlElement = QStringLiteral("%1\n").arg(htmlReplace); } QWebPage page; page.settings()->setAttribute(QWebSettings::JavascriptEnabled, false); page.settings()->setAttribute(QWebSettings::JavaEnabled, false); page.settings()->setAttribute(QWebSettings::PluginsEnabled, false); page.settings()->setAttribute(QWebSettings::AutoLoadImages, false); page.currentFrame()->setHtml(htmlElement); //TODO to be tested/verified if this is not an issue page.settings()->setAttribute(QWebSettings::JavascriptEnabled, true); const QString bodyElement = page.currentFrame()->evaluateJavaScript( QStringLiteral("document.getElementsByTagName('body')[0].innerHTML")).toString(); headElement = page.currentFrame()->evaluateJavaScript( QStringLiteral("document.getElementsByTagName('head')[0].innerHTML")).toString(); page.settings()->setAttribute(QWebSettings::JavascriptEnabled, false); if (!bodyElement.isEmpty()) { if (aStripSignature) { //FIXME strip signature works partially for HTML mails return stripSignature(bodyElement); } return bodyElement; } if (aStripSignature) { //FIXME strip signature works partially for HTML mails return stripSignature(htmlElement); } return htmlElement; } QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString) { QString result; if (wildString.isEmpty()) { return wildString; } unsigned int strLength(wildString.length()); for (uint i = 0; i < strLength;) { QChar ch = wildString[i++]; if (ch == QLatin1Char('%') && i < strLength) { ch = wildString[i++]; switch (ch.toLatin1()) { case 'f': { // sender's initals if (fromDisplayString.isEmpty()) { break; } uint j = 0; const unsigned int strLength(fromDisplayString.length()); for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j) ; for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j) ; result += fromDisplayString[0]; if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) { result += fromDisplayString[j]; } else if (strLength > 1) { if (fromDisplayString[1] > QLatin1Char(' ')) { result += fromDisplayString[1]; } } } break; case '_': result += QLatin1Char(' '); break; case '%': result += QLatin1Char('%'); break; default: result += QLatin1Char('%'); result += ch; break; } } else { result += ch; } } return result; } QString quotedPlainText(const QString &selection, const QString &fromDisplayString) { QString content = selection; // Remove blank lines at the beginning: const int firstNonWS = content.indexOf(QRegExp(QLatin1String("\\S"))); const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS); if (lineStart >= 0) { content.remove(0, static_cast(lineStart)); } const auto quoteString = QStringLiteral("> "); const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString); //FIXME // if (TemplateParserSettings::self()->smartQuote() && mWrap) { // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length()); // } content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr); content.prepend(indentStr); content += QLatin1Char('\n'); return content; } QString quotedHtmlText(const QString &selection) { QString content = selection; //TODO 1) look for all the variations of
and remove the blank lines //2) implement vertical bar for quoted HTML mail. //3) After vertical bar is implemented, If a user wants to edit quoted message, // then the
tags below should open and close as when required. //Add blockquote tag, so that quoted message can be differentiated from normal message content = QLatin1String("
") + content + QLatin1String("
"); return content; } void applyCharset(const KMime::Message::Ptr msg, const KMime::Message::Ptr &origMsg) { // first convert the body from its current encoding to unicode representation QTextCodec *bodyCodec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset())); if (!bodyCodec) { bodyCodec = KCharsets::charsets()->codecForName(QStringLiteral("UTF-8")); } const QString body = bodyCodec->toUnicode(msg->body()); // then apply the encoding of the original message msg->contentType()->setCharset(origMsg->contentType()->charset()); QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset())); if (!codec) { qCritical() << "Could not get text codec for charset" << msg->contentType()->charset(); } else if (!codec->canEncode(body)) { // charset can't encode body, fall back to preferred const QStringList charsets /*= preferredCharsets() */; QList chars; chars.reserve(charsets.count()); foreach (const QString &charset, charsets) { chars << charset.toLatin1(); } //FIXME QByteArray fallbackCharset/* = selectCharset(chars, body)*/; if (fallbackCharset.isEmpty()) { // UTF-8 as fall-through fallbackCharset = "UTF-8"; } codec = KCharsets::charsets()->codecForName(QString::fromLatin1(fallbackCharset)); msg->setBody(codec->fromUnicode(body)); } else { msg->setBody(codec->fromUnicode(body)); } } enum ReplyStrategy { ReplyList, ReplySmart, ReplyAll, ReplyAuthor, ReplyNone }; KMime::Message::Ptr MailTemplates::reply(const KMime::Message::Ptr &origMsg) { //FIXME const bool alwaysPlain = true; //FIXME const ReplyStrategy replyStrategy = ReplySmart; KMime::Message::Ptr msg(new KMime::Message); //FIXME //Personal email addresses KMime::Types::AddrSpecList me; KMime::Types::Mailbox::List toList; KMime::Types::Mailbox::List replyToList; KMime::Types::Mailbox::List mailingListAddresses; // const uint originalIdentity = identityUoid(origMsg); initHeader(msg); replyToList = origMsg->replyTo()->mailboxes(); msg->contentType()->setCharset("utf-8"); if (origMsg->headerByType("List-Post") && origMsg->headerByType("List-Post")->asUnicodeString().contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) { const QString listPost = origMsg->headerByType("List-Post")->asUnicodeString(); QRegExp rx(QStringLiteral("]+)@([^>]+)>"), Qt::CaseInsensitive); if (rx.indexIn(listPost, 0) != -1) { // matched KMime::Types::Mailbox mailbox; mailbox.fromUnicodeString(rx.cap(1) + QLatin1Char('@') + rx.cap(2)); mailingListAddresses << mailbox; } } switch (replyStrategy) { case ReplySmart: { if (auto hdr = origMsg->headerByType("Mail-Followup-To")) { toList << KMime::Types::Mailbox::listFrom7BitString(hdr->as7BitString(false)); } else if (!replyToList.isEmpty()) { toList = replyToList; } else if (!mailingListAddresses.isEmpty()) { toList = (KMime::Types::Mailbox::List() << mailingListAddresses.at(0)); } else { // doesn't seem to be a mailing list, reply to From: address toList = origMsg->from()->mailboxes(); bool listContainsMe = false; for (const auto &m : me) { KMime::Types::Mailbox mailbox; mailbox.setAddress(m); if (toList.contains(mailbox)) { listContainsMe = true; } } if (listContainsMe) { // sender seems to be one of our own identities, so we assume that this // is a reply to a "sent" mail where the users wants to add additional // information for the recipient. toList = origMsg->to()->mailboxes(); } } // strip all my addresses from the list of recipients const KMime::Types::Mailbox::List recipients = toList; toList = stripMyAddressesFromAddressList(recipients, me); // ... unless the list contains only my addresses (reply to self) if (toList.isEmpty() && !recipients.isEmpty()) { toList << recipients.first(); } } break; case ReplyList: { if (auto hdr = origMsg->headerByType("Mail-Followup-To")) { KMime::Types::Mailbox mailbox; mailbox.from7BitString(hdr->as7BitString(false)); toList << mailbox; } else if (!mailingListAddresses.isEmpty()) { toList << mailingListAddresses[ 0 ]; } else if (!replyToList.isEmpty()) { // assume a Reply-To header mangling mailing list toList = replyToList; } //FIXME // strip all my addresses from the list of recipients const KMime::Types::Mailbox::List recipients = toList; toList = stripMyAddressesFromAddressList(recipients, me); } break; case ReplyAll: { KMime::Types::Mailbox::List recipients; KMime::Types::Mailbox::List ccRecipients; // add addresses from the Reply-To header to the list of recipients if (!replyToList.isEmpty()) { recipients = replyToList; // strip all possible mailing list addresses from the list of Reply-To addresses foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) { foreach (const KMime::Types::Mailbox &recipient, recipients) { if (mailbox == recipient) { recipients.removeAll(recipient); } } } } if (!mailingListAddresses.isEmpty()) { // this is a mailing list message if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) { // The sender didn't set a Reply-to address, so we add the From // address to the list of CC recipients. ccRecipients += origMsg->from()->mailboxes(); qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of CC recipients"; } // if it is a mailing list, add the posting address recipients.prepend(mailingListAddresses[ 0 ]); } else { // this is a normal message if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) { // in case of replying to a normal message only then add the From // address to the list of recipients if there was no Reply-to address recipients += origMsg->from()->mailboxes(); qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of recipients"; } } // strip all my addresses from the list of recipients toList = stripMyAddressesFromAddressList(recipients, me); // merge To header and CC header into a list of CC recipients if (!origMsg->cc()->asUnicodeString().isEmpty() || !origMsg->to()->asUnicodeString().isEmpty()) { KMime::Types::Mailbox::List list; if (!origMsg->to()->asUnicodeString().isEmpty()) { list += origMsg->to()->mailboxes(); } if (!origMsg->cc()->asUnicodeString().isEmpty()) { list += origMsg->cc()->mailboxes(); } foreach (const KMime::Types::Mailbox &mailbox, list) { if (!recipients.contains(mailbox) && !ccRecipients.contains(mailbox)) { ccRecipients += mailbox; qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients"; } } } if (!ccRecipients.isEmpty()) { // strip all my addresses from the list of CC recipients ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me); // in case of a reply to self, toList might be empty. if that's the case // then propagate a cc recipient to To: (if there is any). if (toList.isEmpty() && !ccRecipients.isEmpty()) { toList << ccRecipients.at(0); ccRecipients.pop_front(); } foreach (const KMime::Types::Mailbox &mailbox, ccRecipients) { msg->cc()->addAddress(mailbox); } } if (toList.isEmpty() && !recipients.isEmpty()) { // reply to self without other recipients toList << recipients.at(0); } } break; case ReplyAuthor: { if (!replyToList.isEmpty()) { KMime::Types::Mailbox::List recipients = replyToList; // strip the mailing list post address from the list of Reply-To // addresses since we want to reply in private foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) { foreach (const KMime::Types::Mailbox &recipient, recipients) { if (mailbox == recipient) { recipients.removeAll(recipient); } } } if (!recipients.isEmpty()) { toList = recipients; } else { // there was only the mailing list post address in the Reply-To header, // so use the From address instead toList = origMsg->from()->mailboxes(); } } else if (!origMsg->from()->asUnicodeString().isEmpty()) { toList = origMsg->from()->mailboxes(); } } break; case ReplyNone: // the addressees will be set by the caller break; } foreach (const KMime::Types::Mailbox &mailbox, toList) { msg->to()->addAddress(mailbox); } const QByteArray refStr = getRefStr(origMsg); if (!refStr.isEmpty()) { msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } //In-Reply-To = original msg-id msg->inReplyTo()->from7BitString(origMsg->messageID()->as7BitString(false)); msg->subject()->fromUnicodeString(replySubject(origMsg), "utf-8"); auto definedLocale = QLocale::system(); //TODO set empty source instead StringHtmlWriter htmlWriter; QImage paintDevice; CSSHelper cssHelper(&paintDevice); MessageViewer::NodeHelper nodeHelper; ObjectTreeSource source(&htmlWriter, &cssHelper); MessageViewer::ObjectTreeParser otp(&source, &nodeHelper); otp.setAllowAsync(false); otp.parseObjectTree(origMsg.data()); //Add quoted body QString plainBody; QString htmlBody; //On $datetime you wrote: const QDateTime date = origMsg->date()->dateTime(); const auto dateTimeString = QString("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat)).arg(definedLocale.toString(date.time(), QLocale::LongFormat)); const auto onDateYouWroteLine = QString("On %1 you wrote:").arg(dateTimeString); plainBody.append(onDateYouWroteLine); htmlBody.append(plainToHtml(onDateYouWroteLine)); //Strip signature for replies const bool stripSignature = true; //Quoted body QString plainQuote = quotedPlainText(plainMessageText(otp, stripSignature), origMsg->from()->displayString()); if (plainQuote.endsWith(QLatin1Char('\n'))) { plainQuote.chop(1); } plainBody.append(plainQuote); QString headElement; htmlBody.append(quotedHtmlText(htmlMessageText(otp, stripSignature, headElement))); if (alwaysPlain) { htmlBody.clear(); } else { makeValidHtml(htmlBody, headElement); } addProcessedBodyToMessage(msg, plainBody, htmlBody, false); applyCharset(msg, origMsg); msg->assemble(); return msg; }