/*
    Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
    Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
    Copyright (c) 2010 Leo Franchi <lfranchi@kde.org>
    Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com>

    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 <KLocalizedString>
#include <QByteArray>
#include <QDebug>
#include <QDomDocument>
#include <QList>
#include <QStringDecoder>
#include <QSysInfo>
#include <QTextDocument>
#include <QRegularExpression>
#include <functional>

#include <KMime/Types>
#include <KMime/Headers>

#include <MimeTreeParserCore/ObjectTreeParser>

using namespace Qt::Literals::StringLiterals;

QDebug operator<<(QDebug dbg, const KMime::Types::Mailbox &mb)
{
    dbg << mb.addrSpec().asString();
    return dbg;
}

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());
}
}

Message *contentToMessage(Content *content)
{
    content->assemble();
    const auto encoded = content->encodedContent();

    auto message = new Message();
    message->setContent(encoded);
    message->parse();

    return message;
}

}

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;
}

static QString toPlainText(const QString &s)
{
    QTextDocument doc;
    doc.setHtml(s);
    return doc.toPlainText();
}

QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, const QString &newPrefix)
{
    // 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
    const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));
    QRegularExpression rx(bigRegExp, QRegularExpression::CaseInsensitiveOption);
    if (!rx.isValid()) {
        qWarning() << "bigRegExp = \"" << bigRegExp << "\"\n"
                   << "prefix regexp is invalid!";
        qWarning() << "Error: " << rx.errorString() << rx;
        Q_ASSERT(false);
        return str;
    }

    QString tmp = str;
    // We expect a match at the beginning of the string
    QRegularExpressionMatch match;
    if (tmp.indexOf(rx, 0, &match) == 0) {
        return tmp.replace(0, match.capturedLength(), newPrefix + QLatin1Char(' '));
    }
    // No match, we just prefix the newPrefix
    return newPrefix + u' ' + str;
}

const QStringList getForwardPrefixes()
{
    // See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations
    QStringList list;
    // We want to be able to potentially reply to a variety of languages, so only translating is not enough
    list << i18nc("FWD abbreviation for forwarded in emails", "fwd");
    list << u"fwd"_s;
    list << u"fw"_s;
    list << u"wg"_s;
    list << u"vs"_s;
    list << u"tr"_s;
    list << u"rv"_s;
    list << u"enc"_s;
    return list;
}

static QString forwardSubject(const QString &s)
{
    // The standandard prefix
    const QString localPrefix = QStringLiteral("FW:");
    QStringList forwardPrefixes;
    for (const auto &prefix : getForwardPrefixes()) {
        forwardPrefixes << prefix + QStringLiteral("\\s*:");
    }
    return replacePrefixes(s, forwardPrefixes, localPrefix);
}

static QStringList getReplyPrefixes()
{
    // See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations
    QStringList list;
    // We want to be able to potentially reply to a variety of languages, so only translating is not enough
    list << i18nc("RE abbreviation for reply in emails", "re");
    list << u"re"_s;
    list << u"aw"_s;
    list << u"sv"_s;
    list << u"antw"_s;
    list << u"ref"_s;
    return list;
}

static QString replySubject(const QString &s)
{
    // The standandard prefix (latin for "in re", in matter of)
    const QString localPrefix = QStringLiteral("RE:");
    QStringList replyPrefixes;
    for (const auto &prefix : getReplyPrefixes()) {
        replyPrefixes << prefix + u"\\s*:"_s;
        replyPrefixes << prefix + u"\\[.+\\]:"_s;
        replyPrefixes << prefix + u"\\d+:"_s;
    }
    return replacePrefixes(s, replyPrefixes, localPrefix);
}

static QByteArray getRefStr(const QByteArray &references, const QByteArray &messageId)
{
    QByteArray firstRef, lastRef, refStr{references.trimmed()}, retRefStr;
    int i, j;

    if (refStr.isEmpty()) {
        return messageId;
    }

    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 += messageId;
    return retRefStr;
}

KMime::Content *createPlainPartContent(const QString &plainBody, KMime::Content *parent = nullptr)
{
    KMime::Content *textPart = new KMime::Content(parent);
    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 QString &plainBody, const QString &htmlBody, KMime::Message *parent = nullptr)
{
    KMime::Content *multipartAlternative = new KMime::Content(parent);
    multipartAlternative->contentType()->setMimeType("multipart/alternative");
    multipartAlternative->contentType()->setBoundary(KMime::multiPartBoundary());

    KMime::Content *textPart = createPlainPartContent(plainBody, multipartAlternative);
    multipartAlternative->appendContent(textPart);

    KMime::Content *htmlPart = new KMime::Content(multipartAlternative);
    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());
    htmlPart->contentType()->setCharset("utf-8");
    htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
    htmlPart->fromUnicodeString(htmlBody);
    multipartAlternative->appendContent(htmlPart);

    return multipartAlternative;
}

KMime::Content *createMultipartMixedContent(QVector<KMime::Content *> contents)
{
    KMime::Content *multiPartMixed = new KMime::Content();
    multiPartMixed->contentType()->setMimeType("multipart/mixed");
    multiPartMixed->contentType()->setBoundary(KMime::multiPartBoundary());

    for (const auto &content : contents) {
        multiPartMixed->appendContent(content);
    }

    return multiPartMixed;
}

QString plainToHtml(const QString &body)
{
    QString str = body;
    str = str.toHtmlEscaped();
    str.replace(u'\n', u"<br />\n"_s);
    return str;
}

// TODO implement this function using a DOM tree parser
void makeValidHtml(QString &body, const QString &headElement)
{
    QRegularExpression regEx(u"<html.*>"_s, QRegularExpression::InvertedGreedinessOption);

    if (!body.isEmpty() && !body.contains(regEx)) {
        regEx.setPattern(u"<body.*>"_s);
        if (!body.contains(regEx)) {
            body = u"<body>"_s + body + u"<br/></body>"_s;
        }
        regEx.setPattern(u"<head.*>"_s);
        if (!body.contains(regEx)) {
            body = u"<head>"_s + headElement + u"</head>"_s + body;
        }
        body = u"<html>"_s + body + u"</html>"_s;
    }
}

// FIXME strip signature works partially for HTML mails
static QString stripSignature(const QString &msg)
{
    // Following RFC 3676, only > before --
    // I prefer to not delete a SB instead of delete good mail content.
    // We expect no CRLF from the ObjectTreeParser. The regex won't handle it.
    if (msg.contains(QStringLiteral("\r\n"))) {
        qWarning() << "Message contains CRLF, but shouldn't: " << msg;
        Q_ASSERT(false);
    }
    static const QRegularExpression sbDelimiterSearch(u"(^|\n)[> ]*-- \n"_s);
    // The regular expression to look for prefix change
    static const QRegularExpression commonReplySearch(u"^[ ]*>"_s);

    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(u'-', posDeletingStart);
        // The prefix before "-- "$
        if (res.at(posDeletingStart) == u'\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;
}

static void plainMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function<void(const QString &)> &callback)
{
    const auto result = plainTextContent.isEmpty() ? toPlainText(htmlContent) : plainTextContent;
    callback(result);
}

void htmlMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function<void(const QString &body, QString &head)> &callback)
{
    QString htmlElement = htmlContent;

    if (htmlElement.isEmpty()) { // plain mails only
        QString htmlReplace = plainTextContent.toHtmlEscaped();
        htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("<br />"));
        htmlElement = QStringLiteral("<html><head></head><body>%1</body></html>\n").arg(htmlReplace);
    }

    QDomDocument document;
    document.setContent(htmlElement);

    QString body;
    QTextStream bodyStream(&body);
    QString head;
    QTextStream headStream(&head);
    const auto bodies = document.elementsByTagName(u"body"_s);
    const auto heads = document.elementsByTagName(u"head"_s);

    if (bodies.isEmpty()) {
        body = htmlElement;
    } else {
        bodies.item(0).save(bodyStream, 2);
    }
    if (!heads.isEmpty()) {
        heads.item(0).save(headStream, 2);
    }

    callback(body, head);
}

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(QRegularExpression(u"\\S"_s));
    const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS);
    if (lineStart >= 0) {
        content.remove(0, static_cast<unsigned int>(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 <br>  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 <blockquote> tags below should open and close as when required.

    // Add blockquote tag, so that quoted message can be differentiated from normal message
    content = QLatin1String("<blockquote>") + content + QLatin1String("</blockquote>");
    return content;
}

enum ReplyStrategy { ReplyList, ReplySmart, ReplyAll, ReplyAuthor, ReplyNone };

static QByteArray as7BitString(const KMime::Headers::Base *h)
{
    if (h) {
        return h->as7BitString(false);
    }
    return {};
}

static QString asUnicodeString(const KMime::Headers::Base *h)
{
    if (h) {
        return h->asUnicodeString();
    }
    return {};
}

static KMime::Types::Mailbox::List getMailingListAddresses(const KMime::Headers::Base *listPostHeader)
{
    KMime::Types::Mailbox::List mailingListAddresses;
    const QString listPost = asUnicodeString(listPostHeader);
    if (listPost.contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) {
        static QRegularExpression rx(QStringLiteral("<mailto:([^@>]+)@([^>]+)>"), QRegularExpression::CaseInsensitiveOption);
        QRegularExpressionMatch match;
        if (listPost.indexOf(rx, 0, &match) != -1) { // matched
            KMime::Types::Mailbox mailbox;
            mailbox.fromUnicodeString(QString(match.captured(1) + u'@' + match.captured(2)));
            mailingListAddresses << mailbox;
        }
    }
    return mailingListAddresses;
}

struct RecipientMailboxes {
    KMime::Types::Mailbox::List to;
    KMime::Types::Mailbox::List cc;
};

static RecipientMailboxes getRecipients(const KMime::Types::Mailbox::List &from,
                                        const KMime::Types::Mailbox::List &to,
                                        const KMime::Types::Mailbox::List &cc,
                                        const KMime::Types::Mailbox::List &replyToList,
                                        const KMime::Types::Mailbox::List &mailingListAddresses,
                                        const KMime::Types::AddrSpecList &me)
{
    KMime::Types::Mailbox::List toList;
    KMime::Types::Mailbox::List ccList;
    auto listContainsMe = [&](const KMime::Types::Mailbox::List &list) {
        for (const auto &m : me) {
            KMime::Types::Mailbox mailbox;
            mailbox.setAddress(m);
            if (list.contains(mailbox)) {
                return true;
            }
        }
        return false;
    };

    if (listContainsMe(from)) {
        // 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.
        return {to, cc};
    }

    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
        for (const KMime::Types::Mailbox &mailbox : std::as_const(mailingListAddresses)) {
            for (const KMime::Types::Mailbox &recipient : std::as_const(recipients)) {
                if (mailbox == recipient) {
                    recipients.removeAll(recipient);
                }
            }
        }
    }

    if (!mailingListAddresses.isEmpty()) {
        // this is a mailing list message
        if (recipients.isEmpty() && !from.isEmpty()) {
            // The sender didn't set a Reply-to address, so we add the From
            // address to the list of CC recipients.
            ccRecipients += from;
            qDebug() << "Added" << from << "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() && !from.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 += from;
            qDebug() << "Added" << from << "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
    auto appendToCcRecipients = [&](const KMime::Types::Mailbox::List &list) {
        for (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";
            }
        }
    };
    appendToCcRecipients(to);
    appendToCcRecipients(cc);

    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();
        }

        ccList = ccRecipients;
    }

    if (toList.isEmpty() && !recipients.isEmpty()) {
        // reply to self without other recipients
        toList << recipients.at(0);
    }

    return {toList, ccList};
}

void MailTemplates::reply(const KMime::Message::Ptr &origMsg,
                          const std::function<void(const KMime::Message::Ptr &result)> &callback,
                          const KMime::Types::AddrSpecList &me)
{
    // FIXME
    const bool alwaysPlain = true;

    // Decrypt what we have to
    MimeTreeParser::ObjectTreeParser otp;
    otp.parseObjectTree(origMsg.data());
    otp.decryptAndVerify();

    auto partList = otp.collectContentParts();
    if (partList.isEmpty()) {
        Q_ASSERT(false);
        return;
    }
    auto part = partList[0];
    Q_ASSERT(part);

    // Prepare the reply message
    KMime::Message::Ptr msg(new KMime::Message);

    msg->removeHeader<KMime::Headers::To>();
    msg->removeHeader<KMime::Headers::Subject>();
    msg->contentType(true)->setMimeType("text/plain");
    msg->contentType()->setCharset("utf-8");

    auto getMailboxes = [](const KMime::Headers::Base *h) -> KMime::Types::Mailbox::List {
        if (h) {
            return static_cast<const KMime::Headers::Generics::AddressList *>(h)->mailboxes();
        }
        return {};
    };

    auto fromHeader = static_cast<const KMime::Headers::From *>(part->header(KMime::Headers::From::staticType()));
    const auto recipients = getRecipients(fromHeader ? fromHeader->mailboxes() : KMime::Types::Mailbox::List{},
                                          getMailboxes(part->header(KMime::Headers::To::staticType())),
                                          getMailboxes(part->header(KMime::Headers::Cc::staticType())),
                                          getMailboxes(part->header(KMime::Headers::ReplyTo::staticType())),
                                          getMailingListAddresses(part->header("List-Post")),
                                          me);
    for (const auto &mailbox : recipients.to) {
        msg->to()->addAddress(mailbox);
    }
    for (const auto &mailbox : recipients.cc) {
        msg->cc(true)->addAddress(mailbox);
    }

    const auto messageId = as7BitString(part->header(KMime::Headers::MessageID::staticType()));

    const QByteArray refStr = getRefStr(as7BitString(part->header(KMime::Headers::References::staticType())), messageId);
    if (!refStr.isEmpty()) {
        msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr));
    }

    // In-Reply-To = original msg-id
    msg->inReplyTo()->from7BitString(messageId);

    const auto subjectHeader = part->header(KMime::Headers::Subject::staticType());
    msg->subject()->fromUnicodeString(replySubject(asUnicodeString(subjectHeader)));

    auto definedLocale = QLocale::system();

    // Add quoted body
    QString plainBody;
    QString htmlBody;

    // On $datetime you wrote:
    auto dateHeader = static_cast<const KMime::Headers::Date *>(part->header(KMime::Headers::Date::staticType()));
    const QDateTime date = dateHeader ? dateHeader->dateTime() : QDateTime{};
    const auto dateTimeString =
        QStringLiteral("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat), definedLocale.toString(date.time(), QLocale::LongFormat));
    const auto onDateYouWroteLine = i18nc("Reply header", "On %1 you wrote:\n", dateTimeString);
    plainBody.append(onDateYouWroteLine);
    htmlBody.append(plainToHtml(onDateYouWroteLine));

    const auto plainTextContent = otp.plainTextContent();
    const auto htmlContent = otp.htmlContent();

    plainMessageText(plainTextContent, htmlContent, [=](const QString &body) {
        QString result = stripSignature(body);
        // Quoted body
        result = quotedPlainText(result, fromHeader ? fromHeader->displayString() : QString{});
        if (result.endsWith(u'\n')) {
            result.chop(1);
        }
        // The plain body is complete
        QString plainBodyResult = plainBody + result;
        htmlMessageText(plainTextContent, htmlContent, [=](const QString &body, const QString &headElement) {
            QString result = stripSignature(body);

            // The html body is complete
            const auto htmlBodyResult = [&]() {
                if (!alwaysPlain) {
                    QString htmlBodyResult = htmlBody + quotedHtmlText(result);
                    makeValidHtml(htmlBodyResult, headElement);
                    return htmlBodyResult;
                }
                return QString{};
            }();

            // Assemble the message
            msg->removeHeader<KMime::Headers::ContentType>(); // to get rid of old boundary

            KMime::Content *const mainTextPart = htmlBodyResult.isEmpty() ? createPlainPartContent(plainBodyResult, msg.data())
                                                                          : createMultipartAlternativeContent(plainBodyResult, htmlBodyResult, msg.data());
            mainTextPart->assemble();

            msg->setBody(mainTextPart->encodedBody());
            msg->setHeader(mainTextPart->contentType());
            msg->setHeader(mainTextPart->contentTransferEncoding());
            // FIXME this does more harm than good right now.
            msg->assemble();

            callback(msg);
        });
    });
}

void MailTemplates::forward(const KMime::Message::Ptr &origMsg, const std::function<void(const KMime::Message::Ptr &result)> &callback)
{
    MimeTreeParser::ObjectTreeParser otp;
    otp.parseObjectTree(origMsg.data());
    otp.decryptAndVerify();

    KMime::Message::Ptr wrapperMsg(new KMime::Message);
    wrapperMsg->removeHeader<KMime::Headers::To>();
    wrapperMsg->removeHeader<KMime::Headers::Cc>();

    // Decrypt the original message, it will be encrypted again in the composer
    // for the right recipient
    KMime::Message::Ptr forwardedMessage(new KMime::Message());

    if (isEncrypted(origMsg.data())) {
        qDebug() << "Original message was encrypted, decrypting it";

        auto htmlContent = otp.htmlContent();

        KMime::Content *recreatedMsg =
            htmlContent.isEmpty() ? createPlainPartContent(otp.plainTextContent()) : createMultipartAlternativeContent(otp.plainTextContent(), htmlContent);

        KMime::Message::Ptr tmpForwardedMessage;
        auto attachments = otp.collectAttachmentParts();
        if (!attachments.isEmpty()) {
            QVector<KMime::Content *> contents = {recreatedMsg};
            for (const auto &attachment : std::as_const(attachments)) {
                // Copy the node, to avoid deleting the parts node.
                auto c = new KMime::Content;
                c->setContent(attachment->node()->encodedContent());
                c->parse();
                contents.append(c);
            }

            auto msg = createMultipartMixedContent(contents);

            tmpForwardedMessage.reset(KMime::contentToMessage(msg));
        } else {
            tmpForwardedMessage.reset(KMime::contentToMessage(recreatedMsg));
        }

        origMsg->contentType()->fromUnicodeString(tmpForwardedMessage->contentType()->asUnicodeString());
        origMsg->assemble();
        forwardedMessage->setHead(origMsg->head());
        forwardedMessage->setBody(tmpForwardedMessage->encodedBody());
        forwardedMessage->parse();
    } else {
        qDebug() << "Original message was not encrypted, using it as-is";
        forwardedMessage = origMsg;
    }

    auto partList = otp.collectContentParts();
    if (partList.isEmpty()) {
        Q_ASSERT(false);
        callback({});
        return;
    }
    auto part = partList[0];
    Q_ASSERT(part);

    const auto subjectHeader = part->header(KMime::Headers::Subject::staticType());
    const auto subject = asUnicodeString(subjectHeader);

    const QByteArray refStr =
        getRefStr(as7BitString(part->header(KMime::Headers::References::staticType())), as7BitString(part->header(KMime::Headers::MessageID::staticType())));

    wrapperMsg->subject()->fromUnicodeString(forwardSubject(subject));

    if (!refStr.isEmpty()) {
        wrapperMsg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr));
    }

    KMime::Content *fwdAttachment = new KMime::Content;

    fwdAttachment->contentDisposition()->setDisposition(KMime::Headers::CDinline);
    fwdAttachment->contentType()->setMimeType("message/rfc822");
    fwdAttachment->contentDisposition()->setFilename(subject + u".eml"_s);
    fwdAttachment->setBody(KMime::CRLFtoLF(forwardedMessage->encodedContent(false)));

    wrapperMsg->appendContent(fwdAttachment);
    wrapperMsg->assemble();

    callback(wrapperMsg);
}

QString MailTemplates::plaintextContent(const KMime::Message::Ptr &msg)
{
    MimeTreeParser::ObjectTreeParser otp;
    otp.parseObjectTree(msg.data());
    otp.decryptAndVerify();
    const auto plain = otp.plainTextContent();
    if (plain.isEmpty()) {
        // Maybe not as good as the webengine version, but works at least for simple html content
        return toPlainText(otp.htmlContent());
    }
    return plain;
}

QString MailTemplates::body(const KMime::Message::Ptr &msg, bool &isHtml)
{
    MimeTreeParser::ObjectTreeParser otp;
    otp.parseObjectTree(msg.data());
    otp.decryptAndVerify();
    const auto html = otp.htmlContent();
    if (html.isEmpty()) {
        isHtml = false;
        return otp.plainTextContent();
    }
    isHtml = true;
    return html;
}

static KMime::Types::Mailbox::List stringListToMailboxes(const QStringList &list)
{
    KMime::Types::Mailbox::List mailboxes;
    for (const auto &s : list) {
        KMime::Types::Mailbox mb;
        mb.fromUnicodeString(s);
        if (mb.hasAddress()) {
            mailboxes << mb;
        } else {
            qWarning() << "Got an invalid address: " << s << list;
            Q_ASSERT(false);
        }
    }
    return mailboxes;
}

static void setRecipients(KMime::Message &message, const Recipients &recipients)
{
    message.removeHeader<KMime::Headers::To>();
    const auto tos = stringListToMailboxes(recipients.to);
    for (const auto &mb : tos) {
        message.to()->addAddress(mb);
    }
    message.removeHeader<KMime::Headers::Cc>();
    const auto ccs = stringListToMailboxes(recipients.cc);
    for (const auto &mb : ccs) {
        message.cc()->addAddress(mb);
    }
    message.removeHeader<KMime::Headers::Bcc>();
    const auto bccs = stringListToMailboxes(recipients.bcc);
    for (const auto &mb : bccs) {
        message.bcc()->addAddress(mb);
    }
}

KMime::Message::Ptr
MailTemplates::createIMipMessage(const QString &from, const Recipients &recipients, const QString &subject, const QString &body, const QString &attachment)
{
    KMime::Message::Ptr message = KMime::Message::Ptr(new KMime::Message);
    message->removeHeader<KMime::Headers::ContentTransferEncoding>(); // 7Bit, decoded.

    // Set the headers
    message->userAgent()->fromUnicodeString(
        QStringLiteral("%1/%2(%3)").arg(QString::fromLocal8Bit("GPGOL.js")).arg(u"0.1"_s).arg(QSysInfo::prettyProductName()));
    message->from()->fromUnicodeString(from);

    setRecipients(*message, recipients);

    message->date()->setDateTime(QDateTime::currentDateTime());
    message->subject()->fromUnicodeString(subject);
    message->contentType()->setMimeType("multipart/alternative");
    message->contentType()->setBoundary(KMime::multiPartBoundary());

    // Set the first multipart, the body message.
    KMime::Content *bodyMessage = new KMime::Content{message.data()};
    bodyMessage->contentType()->setMimeType("text/plain");
    bodyMessage->contentType()->setCharset("utf-8");
    bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
    bodyMessage->setBody(KMime::CRLFtoLF(body.toUtf8()));
    message->appendContent(bodyMessage);

    // Set the second multipart, the attachment.
    KMime::Content *attachMessage = new KMime::Content{message.data()};
    attachMessage->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
    attachMessage->contentType()->setMimeType("text/calendar");
    attachMessage->contentType()->setCharset("utf-8");
    attachMessage->contentType()->setName(QLatin1String("event.ics"));
    attachMessage->contentType()->setParameter(QByteArray("method"), QLatin1String("REPLY"));
    attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
    attachMessage->setBody(KMime::CRLFtoLF(attachment.toUtf8()));
    message->appendContent(attachMessage);

    // Job done, attach the both multiparts and assemble the message.
    message->assemble();
    return message;
}
