/*
  SPDX-FileCopyrightText: 2009 Constantin Berzan <exit3219@gmail.com>
  SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
  SPDX-FileCopyrightText: 2009 Leo Franchi <lfranchi@kde.org>

  Parts based on KMail code by:

  SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "util.h"
#include "util_p.h"

#include <QRegularExpression>
#include <QStringEncoder>
#include <QTextBlock>
#include <QTextDocument>

#include "editor_debug.h"
#include <KEmailAddress>
#include <KLocalizedString>
#include <KMessageBox>

#include <KMime/Content>
#include <KMime/Headers>
#include <Libkleo/Compliance>

#include "editor/job/composerjob.h"
#include "job/singlepartjob.h"

static QString stripOffPrefixes(const QString &subject)
{
    const QStringList replyPrefixes = {
        QStringLiteral("Re\\s*:"),
        QStringLiteral("Re\\[\\d+\\]:"),
        QStringLiteral("Re\\d+:"),
    };

    const QStringList forwardPrefixes = {
        QStringLiteral("Fwd:"),
        QStringLiteral("FW:<"),
    };

    const QStringList prefixRegExps = replyPrefixes + forwardPrefixes;

    // 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(")|(?:")));

    static QRegularExpression regex;

    if (regex.pattern() != bigRegExp) {
        // the prefixes have changed, so update the regexp
        regex.setPattern(bigRegExp);
        regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
    }

    if (regex.isValid()) {
        QRegularExpressionMatch match = regex.match(subject);
        if (match.hasMatch()) {
            return subject.mid(match.capturedEnd(0));
        }
    } else {
        qCWarning(EDITOR_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n"
                              << "prefix regexp is invalid!";
    }

    return subject;
}

KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret)
{
    MessageComposer::ComposerJob composer;
    MessageComposer::SinglepartJob cteJob(&composer);

    cteJob.contentType()->setMimeType(contentType->mimeType());
    cteJob.contentType()->setCharset(contentType->charset());
    cteJob.setData(encodedBody);
    cteJob.exec();
    cteJob.content()->assemble();

    ret->contentTransferEncoding()->setEncoding(cteJob.contentTransferEncoding()->encoding());
    ret->setBody(cteJob.content()->encodedBody());

    return ret;
}

KMime::Content *MessageComposer::Util::composeHeadersAndBody(KMime::Content *orig,
                                                             QByteArray encodedBody,
                                                             Kleo::CryptoMessageFormat format,
                                                             bool sign,
                                                             const QByteArray &hashAlgo)
{
    auto result = new KMime::Content;

    // called should have tested that the signing/encryption failed
    Q_ASSERT(!encodedBody.isEmpty());

    if (!(format & Kleo::InlineOpenPGPFormat)) { // make a MIME message
        qCDebug(EDITOR_LOG) << "making MIME message, format:" << format;
        makeToplevelContentType(result, format, sign, hashAlgo);

        if (makeMultiMime(format, sign)) { // sign/enc PGPMime, sign SMIME
            const QByteArray boundary = KMime::multiPartBoundary();
            result->contentType()->setBoundary(boundary);

            result->assemble();
            // qCDebug(EDITOR_LOG) << "processed header:" << result->head();

            // Build the encapsulated MIME parts.
            // Build a MIME part holding the code information
            // taking the body contents returned in ciphertext.
            auto code = new KMime::Content;
            setNestedContentType(code, format, sign);
            setNestedContentDisposition(code, format, sign);

            if (sign) { // sign PGPMime, sign SMIME
                if (format & Kleo::AnySMIME) { // sign SMIME
                    auto ct = code->contentTransferEncoding(); // create
                    ct->setEncoding(KMime::Headers::CEbase64);
                    code->setBody(encodedBody);
                } else { // sign PGPMmime
                    setBodyAndCTE(encodedBody, orig->contentType(), code);
                }
                result->appendContent(orig);
                result->appendContent(code);
            } else { // enc PGPMime
                setBodyAndCTE(encodedBody, orig->contentType(), code);

                auto header = new KMime::Headers::Generic("PGP");
                header->fromUnicodeString(QStringLiteral("encrypted"), "utf-8");
                result->setHeader(header);

                auto instruction = new KMime::Content;
                instruction->contentType()->setMimeType("text/plain");
                instruction->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
                if (Kleo::DeVSCompliance::isCompliant()) {
                    instruction->setBody(
                        "This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-Desktop®");
                } else {
                    instruction->setBody(
                        "This message is encrypted with OpenPGP and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or "
                        "Thunderbird");
                }

                auto encryptedPart = new KMime::Content;
                auto ct = encryptedPart->contentType(); // Create
                ct->setMimeType(QByteArrayLiteral("multipart/encrypted"));
                ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted"));
                const QByteArray boundary = KMime::multiPartBoundary();
                ct->setBoundary(boundary);

                // Build a MIME part holding the version information
                // taking the body contents returned in
                // structuring.data.bodyTextVersion.
                auto vers = new KMime::Content;
                vers->contentType()->setMimeType("application/pgp-encrypted");
                vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
                vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
                vers->setBody("Version: 1");

                encryptedPart->appendContent(vers);
                encryptedPart->appendContent(code);

                result->appendContent(instruction);
                result->appendContent(encryptedPart);
            }
        } else { // enc SMIME, sign/enc SMIMEOpaque
            const QByteArray boundary = KMime::multiPartBoundary();
            result->contentType()->setBoundary(boundary);

            result->assemble();
            // qCDebug(EDITOR_LOG) << "processed header:" << result->head();

            // Build the encapsulated MIME parts.
            // Build a MIME part holding the code information
            // taking the body contents returned in ciphertext.
            auto code = new KMime::Content;
            setNestedContentType(code, format, sign);
            setNestedContentDisposition(code, format, sign);

            setBodyAndCTE(encodedBody, orig->contentType(), code);
            code->assemble();
            code->setBody(encodedBody);

            auto instruction = new KMime::Content;
            instruction->contentType()->setMimeType("text/plain");
            instruction->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
            if (Kleo::DeVSCompliance::isCompliant()) {
                instruction->setBody(
                    "This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-Desktop®");
            } else {
                instruction->setBody(
                    "This message is encrypted with S/MIME and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or "
                    "Thunderbird");
            }

            result->appendContent(instruction);
            result->appendContent(code);
        }
    } else { // sign/enc PGPInline
        result->setHead(orig->head());
        result->parse();

        // fixing ContentTransferEncoding
        setBodyAndCTE(encodedBody, orig->contentType(), result);
    }
    return result;
}

// set the correct top-level ContentType on the message
void MessageComposer::Util::makeToplevelContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo)
{
    switch (format) {
    default:
    case Kleo::InlineOpenPGPFormat:
    case Kleo::OpenPGPMIMEFormat: {
        auto ct = content->contentType(); // Create
        if (sign) {
            ct->setMimeType(QByteArrayLiteral("multipart/signed"));
            ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature"));
            ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower());
        } else {
            ct->setMimeType(QByteArrayLiteral("multipart/encrypted"));
            ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted"));
        }
    }
        return;
    case Kleo::SMIMEOpaqueFormat:
    case Kleo::SMIMEFormat: {
        auto ct = content->contentType(); // Create
        if (sign) {
            qCDebug(EDITOR_LOG) << "setting headers for SMIME";
            ct->setMimeType(QByteArrayLiteral("multipart/signed"));
            ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pkcs7-signature"));
            ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(hashAlgo).toLower());
            return;
        } else {
            ct->setMimeType(QByteArrayLiteral("multipart/encrypted"));
            ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/smime-encrypted"));
        }
        // fall through (for encryption, there's no difference between
        // SMIME and SMIMEOpaque, since there is no mp/encrypted for
        // S/MIME)
    }
    }
}

void MessageComposer::Util::setNestedContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign)
{
    switch (format) {
    case Kleo::OpenPGPMIMEFormat: {
        auto ct = content->contentType(); // Create
        if (sign) {
            ct->setMimeType(QByteArrayLiteral("application/pgp-signature"));
            ct->setParameter(QStringLiteral("name"), QStringLiteral("signature.asc"));
            content->contentDescription()->from7BitString("This is a digitally signed message part.");
        } else {
            ct->setMimeType(QByteArrayLiteral("application/octet-stream"));
        }
    }
        return;
    case Kleo::SMIMEFormat: {
        auto ct = content->contentType(); // Create
        if (sign) {
            ct->setMimeType(QByteArrayLiteral("application/pkcs7-signature"));
            ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7s"));
            ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("signed-data"));
        } else {
            auto ct = content->contentType(); // Create
            ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime"));
            ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7m"));
            ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("enveloped-data"));
        }

        return;
    }
    default:
    case Kleo::InlineOpenPGPFormat:
    case Kleo::SMIMEOpaqueFormat:;
    }
}

void MessageComposer::Util::setNestedContentDisposition(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign)
{
    auto ct = content->contentDisposition();
    if (!sign && format & Kleo::OpenPGPMIMEFormat) {
        ct->setDisposition(KMime::Headers::CDinline);
        ct->setFilename(QStringLiteral("msg.asc"));
    } else if (sign && format & Kleo::SMIMEFormat) {
        ct->setDisposition(KMime::Headers::CDattachment);
        ct->setFilename(QStringLiteral("smime.p7s"));
    }
}

bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign)
{
    switch (format) {
    default:
    case Kleo::InlineOpenPGPFormat:
    case Kleo::SMIMEOpaqueFormat:
        return false;
    case Kleo::OpenPGPMIMEFormat:
        return true;
    case Kleo::SMIMEFormat:
        return sign; // only on sign - there's no mp/encrypted for S/MIME
    }
}

QStringList MessageComposer::Util::AttachmentKeywords()
{
    return i18nc(
               "comma-separated list of keywords that are used to detect whether "
               "the user forgot to attach his attachment. Do not add space between words.",
               "attachment,attached")
        .split(QLatin1Char(','));
}

QString MessageComposer::Util::cleanedUpHeaderString(const QString &s)
{
    // remove invalid characters from the header strings
    QString res(s);
    res.remove(QChar::fromLatin1('\r'));
    res.replace(QChar::fromLatin1('\n'), QLatin1Char(' '));
    return res.trimmed();
}

KMime::Content *MessageComposer::Util::findTypeInMessage(KMime::Content *data, const QByteArray &mimeType, const QByteArray &subType)
{
    if (!data->contentType()->isEmpty()) {
        if (mimeType.isEmpty() || subType.isEmpty()) {
            return data;
        }
        if ((mimeType == data->contentType()->mediaType()) && (subType == data->contentType(false)->subType())) {
            return data;
        }
    }

    const auto contents = data->contents();
    for (auto child : contents) {
        if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType()) && (subType == child->contentType()->subType())) {
            return child;
        }
        auto ret = findTypeInMessage(child, mimeType, subType);
        if (ret) {
            return ret;
        }
    }
    return nullptr;
}

bool MessageComposer::Util::hasMissingAttachments(const QStringList &attachmentKeywords, QTextDocument *doc, const QString &subj)
{
    if (!doc) {
        return false;
    }
    QStringList attachWordsList = attachmentKeywords;

    QRegularExpression rx(QLatin1String("\\b") + attachWordsList.join(QLatin1String("\\b|\\b")) + QLatin1String("\\b"),
                          QRegularExpression::CaseInsensitiveOption);

    // check whether the subject contains one of the attachment key words
    // unless the message is a reply or a forwarded message
    bool gotMatch = (stripOffPrefixes(subj) == subj) && (rx.match(subj).hasMatch());

    if (!gotMatch) {
        // check whether the non-quoted text contains one of the attachment key
        // words
        static QRegularExpression quotationRx(QStringLiteral("^([ \\t]*([|>:}#]|[A-Za-z]+>))+"));
        QTextBlock end(doc->end());
        for (QTextBlock it = doc->begin(); it != end; it = it.next()) {
            const QString line = it.text();
            gotMatch = (!quotationRx.match(line).hasMatch()) && (rx.match(line).hasMatch());
            if (gotMatch) {
                break;
            }
        }
    }

    if (!gotMatch) {
        return false;
    }
    return true;
}

static QStringList encodeIdn(const QStringList &emails)
{
    QStringList encoded;
    encoded.reserve(emails.count());
    for (const QString &email : emails) {
        encoded << KEmailAddress::normalizeAddressesAndEncodeIdn(email);
    }
    return encoded;
}

QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails)
{
    QStringList clean;
    clean.reserve(emails.count());
    for (const QString &email : emails) {
        clean << KEmailAddress::extractEmailAddress(email);
    }
    return clean;
}

QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails)
{
    return cleanEmailList(encodeIdn(emails));
}

void MessageComposer::Util::addCustomHeaders(const KMime::Message::Ptr &message, const QMap<QByteArray, QString> &custom)
{
    QMapIterator<QByteArray, QString> customHeader(custom);
    while (customHeader.hasNext()) {
        customHeader.next();
        auto header = new KMime::Headers::Generic(customHeader.key().constData());
        header->fromUnicodeString(customHeader.value(), "utf-8");
        message->setHeader(header);
    }
}
