/* -*- mode: c++; c-basic-offset:4 -*-
    decryptverifytask.cpp

    This file is part of Kleopatra, the KDE keymanager
    SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB

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

#include <config-kleopatra.h>

#include "decryptverifytask.h"

#include <QGpgME/DecryptJob>
#include <QGpgME/DecryptVerifyArchiveJob>
#include <QGpgME/DecryptVerifyJob>
#include <QGpgME/Protocol>
#include <QGpgME/VerifyDetachedJob>
#include <QGpgME/VerifyOpaqueJob>

#include <Libkleo/AuditLogEntry>
#include <Libkleo/Classify>
#include <Libkleo/Compliance>
#include <Libkleo/Dn>
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <Libkleo/KleoException>
#include <Libkleo/Predicates>
#include <Libkleo/Stl_Util>

#include <Libkleo/GnuPG>
#include <utils/detail_p.h>
#include <utils/input.h>
#include <utils/kleo_assert.h>
#include <utils/output.h>

#include <KMime/HeaderParsing>

#include <gpgme++/context.h>
#include <gpgme++/decryptionresult.h>
#include <gpgme++/error.h>
#include <gpgme++/key.h>
#include <gpgme++/verificationresult.h>

#include <gpg-error.h>

#include "kleopatra_debug.h"

#include <KFileUtils>
#include <KLocalizedString>

#include <QByteArray>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QIODevice>
#include <QLocale>
#include <QMimeDatabase>
#include <QStringList>
#include <QTextDocument> // Qt::escape

#include <algorithm>
#include <sstream>

using namespace Kleo::Crypto;
using namespace Kleo;
using namespace GpgME;
using namespace KMime::Types;

namespace
{

static AuditLogEntry auditLogFromSender(QObject *sender)
{
    return AuditLogEntry::fromJob(qobject_cast<const QGpgME::Job *>(sender));
}

static bool addrspec_equal(const AddrSpec &lhs, const AddrSpec &rhs, Qt::CaseSensitivity cs)
{
    return lhs.localPart.compare(rhs.localPart, cs) == 0 && lhs.domain.compare(rhs.domain, Qt::CaseInsensitive) == 0;
}

static bool mailbox_equal(const Mailbox &lhs, const Mailbox &rhs, Qt::CaseSensitivity cs)
{
    return addrspec_equal(lhs.addrSpec(), rhs.addrSpec(), cs);
}

static std::string stripAngleBrackets(const std::string &str)
{
    if (str.empty()) {
        return str;
    }
    if (str[0] == '<' && str[str.size() - 1] == '>') {
        return str.substr(1, str.size() - 2);
    }
    return str;
}

static std::string email(const UserID &uid)
{
    if (uid.parent().protocol() == OpenPGP) {
        if (const char *const email = uid.email()) {
            return stripAngleBrackets(email);
        } else {
            return std::string();
        }
    }

    Q_ASSERT(uid.parent().protocol() == CMS);

    if (const char *const id = uid.id())
        if (*id == '<') {
            return stripAngleBrackets(id);
        } else {
            return DN(id)[QStringLiteral("EMAIL")].trimmed().toUtf8().constData();
        }
    else {
        return std::string();
    }
}

static Mailbox mailbox(const UserID &uid)
{
    const std::string e = email(uid);
    Mailbox mbox;
    if (!e.empty()) {
        mbox.setAddress(e.c_str());
    }
    return mbox;
}

static std::vector<Mailbox> extractMailboxes(const Key &key)
{
    std::vector<Mailbox> res;
    const auto userIDs{key.userIDs()};
    for (const UserID &id : userIDs) {
        const Mailbox mbox = mailbox(id);
        if (!mbox.addrSpec().isEmpty()) {
            res.push_back(mbox);
        }
    }
    return res;
}

static std::vector<Mailbox> extractMailboxes(const std::vector<Key> &signers)
{
    std::vector<Mailbox> res;
    for (const Key &i : signers) {
        const std::vector<Mailbox> bxs = extractMailboxes(i);
        res.insert(res.end(), bxs.begin(), bxs.end());
    }
    return res;
}

static bool keyContainsMailbox(const Key &key, const Mailbox &mbox)
{
    const std::vector<Mailbox> mbxs = extractMailboxes(key);
    return std::find_if(mbxs.cbegin(),
                        mbxs.cend(),
                        [mbox](const Mailbox &m) {
                            return mailbox_equal(mbox, m, Qt::CaseInsensitive);
                        })
        != mbxs.cend();
}

static bool keysContainMailbox(const std::vector<Key> &keys, const Mailbox &mbox)
{
    return std::find_if(keys.cbegin(),
                        keys.cend(),
                        [mbox](const Key &key) {
                            return keyContainsMailbox(key, mbox);
                        })
        != keys.cend();
}

static bool relevantInDecryptVerifyContext(const VerificationResult &r)
{
    // for D/V operations, we ignore verification results which are not errors and contain
    // no signatures (which means that the data was just not signed)

    return (r.error() && r.error().code() != GPG_ERR_DECRYPT_FAILED) || r.numSignatures() > 0;
}

static QString signatureSummaryToString(int summary)
{
    if (summary & Signature::None) {
        return i18n("Error: Signature not verified");
    } else if (summary & Signature::Valid || summary & Signature::Green) {
        return i18n("Good signature");
    } else if (summary & Signature::KeyRevoked) {
        return i18n("Signing certificate was revoked");
    } else if (summary & Signature::KeyExpired) {
        return i18n("Signing certificate is expired");
    } else if (summary & Signature::KeyMissing) {
        return i18n("Certificate is not available");
    } else if (summary & Signature::SigExpired) {
        return i18n("Signature expired");
    } else if (summary & Signature::CrlMissing) {
        return i18n("CRL missing");
    } else if (summary & Signature::CrlTooOld) {
        return i18n("CRL too old");
    } else if (summary & Signature::BadPolicy) {
        return i18n("Bad policy");
    } else if (summary & Signature::SysError) {
        return i18n("System error"); // ### retrieve system error details?
    } else if (summary & Signature::Red) {
        return i18n("Bad signature");
    }
    return QString();
}

static QString formatValidSignatureWithTrustLevel(const UserID &id)
{
    if (id.isNull()) {
        return QString();
    }
    switch (id.validity()) {
    case UserID::Marginal:
        return i18n("The signature is valid but the trust in the certificate's validity is only marginal.");
    case UserID::Full:
        return i18n("The signature is valid and the certificate's validity is fully trusted.");
    case UserID::Ultimate:
        return i18n("The signature is valid and the certificate's validity is ultimately trusted.");
    case UserID::Never:
        return i18n("The signature is valid but the certificate's validity is <em>not trusted</em>.");
    case UserID::Unknown:
        return i18n("The signature is valid but the certificate's validity is unknown.");
    case UserID::Undefined:
    default:
        return i18n("The signature is valid but the certificate's validity is undefined.");
    }
}

static QString renderKeyLink(const QString &fpr, const QString &text)
{
    return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(fpr, text);
}

static QString renderKey(const Key &key)
{
    if (key.isNull()) {
        return i18n("Unknown certificate");
    }

    if (key.primaryFingerprint() && strlen(key.primaryFingerprint()) > 16 && key.numUserIDs()) {
        const QString text = QStringLiteral("%1 (%2)")
                                 .arg(Formatting::prettyNameAndEMail(key).toHtmlEscaped())
                                 .arg(Formatting::prettyID(QString::fromLocal8Bit(key.primaryFingerprint()).right(16).toLatin1().constData()));
        return renderKeyLink(QLatin1String(key.primaryFingerprint()), text);
    }

    return renderKeyLink(QLatin1String(key.primaryFingerprint()), Formatting::prettyID(key.primaryFingerprint()));
}

static QString renderKeyEMailOnlyNameAsFallback(const Key &key)
{
    if (key.isNull()) {
        return i18n("Unknown certificate");
    }
    const QString email = Formatting::prettyEMail(key);
    const QString user = !email.isEmpty() ? email : Formatting::prettyName(key);
    return renderKeyLink(QLatin1String(key.primaryFingerprint()), user);
}

static QString formatDate(const QDateTime &dt)
{
    return QLocale().toString(dt);
}
static QString formatSigningInformation(const Signature &sig)
{
    if (sig.isNull()) {
        return QString();
    }
    const QDateTime dt = sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime();
    QString text;
    Key key = sig.key();
    if (dt.isValid()) {
        text = i18nc("1 is a date", "Signature created on %1", formatDate(dt)) + QStringLiteral("<br>");
    }
    if (key.isNull()) {
        return text += i18n("With unavailable certificate:") + QStringLiteral("<br>ID: 0x%1").arg(QString::fromLatin1(sig.fingerprint()).toUpper());
    }
    text += i18n("With certificate:") + QStringLiteral("<br>") + renderKey(key);

    if (DeVSCompliance::isCompliant()) {
        text += (QStringLiteral("<br/>")
                 + (sig.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
                                         "The signature is %1",
                                         DeVSCompliance::name(true))
                                 : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
                                         "The signature <b>is not</b> %1.",
                                         DeVSCompliance::name(true))));
    }

    return text;
}

static QString strikeOut(const QString &str, bool strike)
{
    return QString(strike ? QStringLiteral("<s>%1</s>") : QStringLiteral("%1")).arg(str.toHtmlEscaped());
}

static QString formatInputOutputLabel(const QString &input, const QString &output, bool inputDeleted, bool outputDeleted)
{
    if (output.isEmpty()) {
        return strikeOut(input, inputDeleted);
    }
    return i18nc("Input file --> Output file (rarr is arrow", "%1 &rarr; %2", strikeOut(input, inputDeleted), strikeOut(output, outputDeleted));
}

static bool IsErrorOrCanceled(const GpgME::Error &err)
{
    return err || err.isCanceled();
}

static bool IsErrorOrCanceled(const Result &res)
{
    return IsErrorOrCanceled(res.error());
}

static bool IsBad(const Signature &sig)
{
    return sig.summary() & Signature::Red;
}

static bool IsGoodOrValid(const Signature &sig)
{
    return (sig.summary() & Signature::Valid) || (sig.summary() & Signature::Green);
}

static UserID findUserIDByMailbox(const Key &key, const Mailbox &mbox)
{
    const auto userIDs{key.userIDs()};
    for (const UserID &id : userIDs)
        if (mailbox_equal(mailbox(id), mbox, Qt::CaseInsensitive)) {
            return id;
        }
    return UserID();
}

static void updateKeys(const VerificationResult &result)
{
    // This little hack works around the problem that GnuPG / GpgME does not
    // provide Key information in a verification result. The Key object is
    // a dummy just holding the KeyID. This hack ensures that all available
    // keys are fetched from the backend and are populated
    for (const auto &sig : result.signatures()) {
        // Update key information
        sig.key(true, true);
    }
}

static QString ensureUniqueDirectory(const QString &path)
{
    // make sure that we don't use an existing directory
    QString uniquePath = path;
    const QFileInfo outputInfo{path};
    if (outputInfo.exists()) {
        const auto uniqueName = KFileUtils::suggestName(QUrl::fromLocalFile(outputInfo.absolutePath()), outputInfo.fileName());
        uniquePath = outputInfo.dir().filePath(uniqueName);
    }
    if (!QDir{}.mkpath(uniquePath)) {
        return {};
    }
    return uniquePath;
}

static bool mimeTypeInherits(const QMimeType &mimeType, const QString &mimeTypeName)
{
    // inherits is expensive on an invalid mimeType
    return mimeType.isValid() && mimeType.inherits(mimeTypeName);
}

}

class DecryptVerifyResult::SenderInfo
{
public:
    explicit SenderInfo(const Mailbox &infSender, const std::vector<Key> &signers_)
        : informativeSender(infSender)
        , signers(signers_)
    {
    }
    const Mailbox informativeSender;
    const std::vector<Key> signers;
    bool hasInformativeSender() const
    {
        return !informativeSender.addrSpec().isEmpty();
    }
    bool conflicts() const
    {
        return hasInformativeSender() && hasKeys() && !keysContainMailbox(signers, informativeSender);
    }
    bool hasKeys() const
    {
        return std::any_of(signers.cbegin(), signers.cend(), [](const Key &key) {
            return !key.isNull();
        });
    }
    std::vector<Mailbox> signerMailboxes() const
    {
        return extractMailboxes(signers);
    }
};

namespace
{

static Task::Result::VisualCode codeForVerificationResult(const VerificationResult &res)
{
    if (res.isNull()) {
        return Task::Result::NeutralSuccess;
    }

    const std::vector<Signature> sigs = res.signatures();
    if (sigs.empty()) {
        return Task::Result::Warning;
    }

    if (std::find_if(sigs.begin(), sigs.end(), IsBad) != sigs.end()) {
        return Task::Result::Danger;
    }

    if ((size_t)std::count_if(sigs.begin(), sigs.end(), IsGoodOrValid) == sigs.size()) {
        return Task::Result::AllGood;
    }

    return Task::Result::Warning;
}

static QString formatVerificationResultOverview(const VerificationResult &res, const DecryptVerifyResult::SenderInfo &info)
{
    if (res.isNull()) {
        return QString();
    }

    const Error err = res.error();

    if (err.isCanceled()) {
        return i18n("<b>Verification canceled.</b>");
    } else if (err) {
        return i18n("<b>Verification failed: %1.</b>", Formatting::errorAsString(err).toHtmlEscaped());
    }

    const std::vector<Signature> sigs = res.signatures();

    if (sigs.empty()) {
        return i18n("<b>No signatures found.</b>");
    }

    const uint bad = std::count_if(sigs.cbegin(), sigs.cend(), IsBad);
    if (bad > 0) {
        return i18np("<b>Invalid signature.</b>", "<b>%1 invalid signatures.</b>", bad);
    }
    const uint warn = std::count_if(sigs.cbegin(), sigs.cend(), [](const Signature &sig) {
        return !IsGoodOrValid(sig);
    });
    if (warn == sigs.size()) {
        return i18np("<b>The data could not be verified.</b>", "<b>%1 signatures could not be verified.</b>", warn);
    }

    // Good signature:
    QString text;
    if (sigs.size() == 1) {
        text = i18n("<b>Valid signature by %1</b>", renderKeyEMailOnlyNameAsFallback(sigs[0].key()));
        if (info.conflicts())
            text += i18n("<br/><b>Warning:</b> The sender's mail address is not stored in the %1 used for signing.",
                         renderKeyLink(QLatin1String(sigs[0].key().primaryFingerprint()), i18n("certificate")));
    } else {
        text = i18np("<b>Valid signature.</b>", "<b>%1 valid signatures.</b>", sigs.size());
        if (info.conflicts()) {
            text += i18n("<br/><b>Warning:</b> The sender's mail address is not stored in the certificates used for signing.");
        }
    }

    return text;
}

static QString formatDecryptionResultOverview(const DecryptionResult &result, const QString &errorString = QString())
{
    const Error err = result.error();

    if (err.isCanceled()) {
        return i18n("<b>Decryption canceled.</b>");
    } else if (result.error().code() == GPG_ERR_NO_SECKEY) {
        return i18nc("@info",
                     "<b>Decryption not possible: %1</b><br />The data was not encrypted for any secret key in your certificate list.",
                     Formatting::errorAsString(err));
    } else if (result.isLegacyCipherNoMDC()) {
        return i18n("<b>Decryption failed: %1.</b>", i18n("No integrity protection (MDC)."));
    } else if (!errorString.isEmpty()) {
        return i18n("<b>Decryption failed: %1.</b>", errorString.toHtmlEscaped());
    } else if (err) {
        return i18n("<b>Decryption failed: %1.</b>", Formatting::errorAsString(err));
    }
    return i18n("<b>Decryption succeeded.</b>");
}

static QString formatSignature(const Signature &sig, const DecryptVerifyResult::SenderInfo &info)
{
    if (sig.isNull()) {
        return QString();
    }

    const QString text = formatSigningInformation(sig) + QLatin1String("<br/>");
    const Key key = sig.key();

    // Green
    if (sig.summary() & Signature::Valid) {
        const UserID id = findUserIDByMailbox(key, info.informativeSender);
        return text + formatValidSignatureWithTrustLevel(!id.isNull() ? id : key.userID(0));
    }

    // Red
    if ((sig.summary() & Signature::Red)) {
        const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
        if (sig.summary() & Signature::SysError) {
            return ret + QStringLiteral(" (%1)").arg(Formatting::errorAsString(sig.status()));
        }
        return ret;
    }

    // Key missing
    if ((sig.summary() & Signature::KeyMissing)) {
        return text + i18n("You can search the certificate on a keyserver or import it from a file.");
    }

    // Yellow
    if ((sig.validity() & Signature::Validity::Undefined) //
        || (sig.validity() & Signature::Validity::Unknown) //
        || (sig.summary() == Signature::Summary::None)) {
        return text
            + (key.protocol() == OpenPGP
                   ? i18n("The used key is not certified by you or any trusted person.")
                   : i18n("The used certificate is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown."));
    }

    // Catch all fall through
    const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
    if (sig.summary() & Signature::SysError) {
        return ret + QStringLiteral(" (%1)").arg(Formatting::errorAsString(sig.status()));
    }
    return ret;
}

static QStringList format(const std::vector<Mailbox> &mbxs)
{
    QStringList res;
    std::transform(mbxs.cbegin(), mbxs.cend(), std::back_inserter(res), [](const Mailbox &mbox) {
        return mbox.prettyAddress();
    });
    return res;
}

static QString formatVerificationResultDetails(const VerificationResult &res, const DecryptVerifyResult::SenderInfo &info, const QString &errorString)
{
    if ((res.error().code() == GPG_ERR_EIO || res.error().code() == GPG_ERR_NO_DATA) && !errorString.isEmpty()) {
        return i18n("Input error: %1", errorString);
    }

    const std::vector<Signature> sigs = res.signatures();
    QString details;
    for (const Signature &sig : sigs) {
        details += formatSignature(sig, info) + QLatin1Char('\n');
    }
    details = details.trimmed();
    details.replace(QLatin1Char('\n'), QStringLiteral("<br/><br/>"));
    if (info.conflicts()) {
        details += i18n("<p>The sender's address %1 is not stored in the certificate. Stored: %2</p>",
                        info.informativeSender.prettyAddress(),
                        format(info.signerMailboxes()).join(i18nc("separator for a list of e-mail addresses", ", ")));
    }
    return details;
}

static QString formatRecipientsDetails(const std::vector<Key> &knownRecipients, unsigned int numRecipients)
{
    if (numRecipients == 0) {
        return {};
    }

    QString details = i18np("Recipient:", "Recipients:", numRecipients);

    if (numRecipients == 1) {
        if (knownRecipients.empty()) {
            details += QLatin1Char(' ') + i18nc("@info", "One unknown recipient");
        } else {
            details += QLatin1Char(' ') + Formatting::summaryLine(knownRecipients.front()).toHtmlEscaped();
        }
    } else {
        details += QLatin1String("<ul>");
        for (const Key &key : knownRecipients) {
            details += QLatin1String("<li>") + Formatting::summaryLine(key).toHtmlEscaped() + QLatin1String("</li>");
        }
        if (knownRecipients.size() < numRecipients) {
            details += QLatin1String("<li>") + i18np("One unknown recipient", "%1 unknown recipients", numRecipients - knownRecipients.size())
                + QLatin1String("</li>");
        }
        details += QLatin1String("</ul>");
    }

    return details;
}

static QString formatDecryptionResultDetails(const DecryptionResult &res,
                                             const std::vector<Key> &recipients,
                                             const QString &errorString,
                                             bool isSigned,
                                             const QPointer<Task> &task)
{
    if ((res.error().code() == GPG_ERR_EIO || res.error().code() == GPG_ERR_NO_DATA) && !errorString.isEmpty()) {
        return i18n("Input error: %1", errorString);
    }

    if (res.isNull() || res.error() || res.error().isCanceled()) {
        if (res.error().code() == GPG_ERR_NO_SECKEY && recipients.empty()) {
            return {};
        }
        return formatRecipientsDetails(recipients, res.numRecipients());
    }

    QString details;

    if (DeVSCompliance::isCompliant()) {
        details += ((res.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
                                          "The decryption is %1.",
                                          DeVSCompliance::name(true))
                                  : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
                                          "The decryption <b>is not</b> %1.",
                                          DeVSCompliance::name(true)))
                    + QStringLiteral("<br/>"));
    }

    if (res.fileName()) {
        const auto decVerifyTask = qobject_cast<AbstractDecryptVerifyTask *>(task.data());
        if (decVerifyTask) {
            const auto embedFileName = QString::fromUtf8(res.fileName()).toHtmlEscaped();

            if (embedFileName != decVerifyTask->outputLabel()) {
                details += i18n("Embedded file name: '%1'", embedFileName);
                details += QStringLiteral("<br/>");
            }
        }
    }

    if (!isSigned) {
        details += i18n("<b>Note:</b> You cannot be sure who encrypted this message as it is not signed.") + QLatin1String("<br/>");
    }

    if (res.isLegacyCipherNoMDC()) {
        details += i18nc("Integrity protection was missing because an old cipher was used.",
                         "<b>Hint:</b> If this file was encrypted before the year 2003 it is "
                         "likely that the file is legitimate.  This is because back "
                         "then integrity protection was not widely used.")
            + QStringLiteral("<br/><br/>")
            + i18nc("The user is offered to force decrypt a non integrity protected message. With the strong advice to re-encrypt it.",
                    "If you are confident that the file was not manipulated you should re-encrypt it after you have forced the decryption.")
            + QStringLiteral("<br/><br/>");
    }

    details += formatRecipientsDetails(recipients, res.numRecipients());

    return details;
}

static QString formatDecryptVerifyResultOverview(const DecryptionResult &dr, const VerificationResult &vr, const DecryptVerifyResult::SenderInfo &info)
{
    if (IsErrorOrCanceled(dr) || !relevantInDecryptVerifyContext(vr)) {
        return formatDecryptionResultOverview(dr);
    }
    return formatVerificationResultOverview(vr, info);
}

static QString formatDecryptVerifyResultDetails(const DecryptionResult &dr,
                                                const VerificationResult &vr,
                                                const std::vector<Key> &recipients,
                                                const DecryptVerifyResult::SenderInfo &info,
                                                const QString &errorString,
                                                const QPointer<Task> &task)
{
    const QString drDetails = formatDecryptionResultDetails(dr, recipients, errorString, relevantInDecryptVerifyContext(vr), task);
    if (IsErrorOrCanceled(dr) || !relevantInDecryptVerifyContext(vr)) {
        return drDetails;
    }
    return drDetails + (drDetails.isEmpty() ? QString() : QStringLiteral("<br/>")) + formatVerificationResultDetails(vr, info, errorString);
}

} // anon namespace

class DecryptVerifyResult::Private
{
    DecryptVerifyResult *const q;

public:
    Private(DecryptVerifyOperation type,
            const VerificationResult &vr,
            const DecryptionResult &dr,
            const QByteArray &stuff,
            const QString &fileName,
            const GpgME::Error &error,
            const QString &errString,
            const QString &input,
            const QString &output,
            const AuditLogEntry &auditLog,
            const Mailbox &informativeSender,
            DecryptVerifyResult *qq)
        : q(qq)
        , m_type(type)
        , m_verificationResult(vr)
        , m_decryptionResult(dr)
        , m_stuff(stuff)
        , m_fileName(fileName)
        , m_error(error)
        , m_errorString(errString)
        , m_inputLabel(input)
        , m_outputLabel(output)
        , m_auditLog(auditLog)
        , m_informativeSender(informativeSender)
    {
    }

    QString label() const
    {
        return formatInputOutputLabel(m_inputLabel, m_outputLabel, false, q->hasError());
    }

    DecryptVerifyResult::SenderInfo makeSenderInfo() const;

    bool isDecryptOnly() const
    {
        return m_type == Decrypt;
    }
    bool isVerifyOnly() const
    {
        return m_type == Verify;
    }
    bool isDecryptVerify() const
    {
        return m_type == DecryptVerify;
    }
    DecryptVerifyOperation m_type;
    VerificationResult m_verificationResult;
    DecryptionResult m_decryptionResult;
    QByteArray m_stuff;
    QString m_fileName;
    GpgME::Error m_error;
    QString m_errorString;
    QString m_inputLabel;
    QString m_outputLabel;
    const AuditLogEntry m_auditLog;
    const Mailbox m_informativeSender;
};

DecryptVerifyResult::SenderInfo DecryptVerifyResult::Private::makeSenderInfo() const
{
    return SenderInfo(m_informativeSender, KeyCache::instance()->findSigners(m_verificationResult));
}

std::shared_ptr<DecryptVerifyResult>
AbstractDecryptVerifyTask::fromDecryptResult(const DecryptionResult &dr, const QByteArray &plaintext, const AuditLogEntry &auditLog)
{
    return std::shared_ptr<DecryptVerifyResult>(new DecryptVerifyResult(Decrypt, //
                                                                        VerificationResult(),
                                                                        dr,
                                                                        plaintext,
                                                                        {},
                                                                        {},
                                                                        QString(),
                                                                        inputLabel(),
                                                                        outputLabel(),
                                                                        auditLog,
                                                                        this,
                                                                        informativeSender()));
}

std::shared_ptr<DecryptVerifyResult> AbstractDecryptVerifyTask::fromDecryptResult(const GpgME::Error &err, const QString &what, const AuditLogEntry &auditLog)
{
    return std::shared_ptr<DecryptVerifyResult>(new DecryptVerifyResult(Decrypt, //
                                                                        VerificationResult(),
                                                                        DecryptionResult(err),
                                                                        QByteArray(),
                                                                        {},
                                                                        err,
                                                                        what,
                                                                        inputLabel(),
                                                                        outputLabel(),
                                                                        auditLog,
                                                                        this,
                                                                        informativeSender()));
}

std::shared_ptr<DecryptVerifyResult> AbstractDecryptVerifyTask::fromDecryptVerifyResult(const DecryptionResult &dr,
                                                                                        const VerificationResult &vr,
                                                                                        const QByteArray &plaintext,
                                                                                        const QString &fileName,
                                                                                        const AuditLogEntry &auditLog)
{
    const auto err = dr.error().code() ? dr.error() : vr.error();
    return std::shared_ptr<DecryptVerifyResult>(new DecryptVerifyResult(DecryptVerify, //
                                                                        vr,
                                                                        dr,
                                                                        plaintext,
                                                                        fileName,
                                                                        err,
                                                                        QString(),
                                                                        inputLabel(),
                                                                        outputLabel(),
                                                                        auditLog,
                                                                        this,
                                                                        informativeSender()));
}

std::shared_ptr<DecryptVerifyResult>
AbstractDecryptVerifyTask::fromDecryptVerifyResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog)
{
    return std::shared_ptr<DecryptVerifyResult>(new DecryptVerifyResult(DecryptVerify, //
                                                                        VerificationResult(),
                                                                        DecryptionResult(err),
                                                                        QByteArray(),
                                                                        {},
                                                                        err,
                                                                        details,
                                                                        inputLabel(),
                                                                        outputLabel(),
                                                                        auditLog,
                                                                        this,
                                                                        informativeSender()));
}

std::shared_ptr<DecryptVerifyResult>
AbstractDecryptVerifyTask::fromVerifyOpaqueResult(const VerificationResult &vr, const QByteArray &plaintext, const AuditLogEntry &auditLog)
{
    return std::shared_ptr<DecryptVerifyResult>(new DecryptVerifyResult(Verify, //
                                                                        vr,
                                                                        DecryptionResult(),
                                                                        plaintext,
                                                                        {},
                                                                        {},
                                                                        QString(),
                                                                        inputLabel(),
                                                                        outputLabel(),
                                                                        auditLog,
                                                                        this,
                                                                        informativeSender()));
}
std::shared_ptr<DecryptVerifyResult>
AbstractDecryptVerifyTask::fromVerifyOpaqueResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog)
{
    return std::shared_ptr<DecryptVerifyResult>(new DecryptVerifyResult(Verify, //
                                                                        VerificationResult(err),
                                                                        DecryptionResult(),
                                                                        QByteArray(),
                                                                        {},
                                                                        err,
                                                                        details,
                                                                        inputLabel(),
                                                                        outputLabel(),
                                                                        auditLog,
                                                                        this,
                                                                        informativeSender()));
}

std::shared_ptr<DecryptVerifyResult> AbstractDecryptVerifyTask::fromVerifyDetachedResult(const VerificationResult &vr, const AuditLogEntry &auditLog)
{
    return std::shared_ptr<DecryptVerifyResult>(new DecryptVerifyResult(Verify, //
                                                                        vr,
                                                                        DecryptionResult(),
                                                                        QByteArray(),
                                                                        {},
                                                                        {},
                                                                        QString(),
                                                                        inputLabel(),
                                                                        outputLabel(),
                                                                        auditLog,
                                                                        this,
                                                                        informativeSender()));
}
std::shared_ptr<DecryptVerifyResult>
AbstractDecryptVerifyTask::fromVerifyDetachedResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog)
{
    return std::shared_ptr<DecryptVerifyResult>(new DecryptVerifyResult(Verify, //
                                                                        VerificationResult(err),
                                                                        DecryptionResult(),
                                                                        QByteArray(),
                                                                        {},
                                                                        err,
                                                                        details,
                                                                        inputLabel(),
                                                                        outputLabel(),
                                                                        auditLog,
                                                                        this,
                                                                        informativeSender()));
}

DecryptVerifyResult::DecryptVerifyResult(DecryptVerifyOperation type,
                                         const VerificationResult &vr,
                                         const DecryptionResult &dr,
                                         const QByteArray &stuff,
                                         const QString &fileName,
                                         const GpgME::Error &error,
                                         const QString &errString,
                                         const QString &inputLabel,
                                         const QString &outputLabel,
                                         const AuditLogEntry &auditLog,
                                         Task *parentTask,
                                         const Mailbox &informativeSender)
    : Task::Result(parentTask)
    , d(new Private(type, vr, dr, stuff, fileName, error, errString, inputLabel, outputLabel, auditLog, informativeSender, this))
{
}

Task::Result::ContentType DecryptVerifyResult::viewableContentType() const
{
#if QGPGME_SUPPORTS_IS_MIME
    if (decryptionResult().isMime()) {
        return Task::Result::ContentType::Mime;
    }
#endif

    if (fileName().endsWith(QStringLiteral("openpgp-encrypted-message"))) {
        return Task::Result::ContentType::Mime;
    }

    QMimeDatabase mimeDatabase;
    const auto mimeType = mimeDatabase.mimeTypeForFile(fileName());
    if (mimeTypeInherits(mimeType, QStringLiteral("message/rfc822"))) {
        return Task::Result::ContentType::Mime;
    }

    if (mimeTypeInherits(mimeType, QStringLiteral("application/mbox"))) {
        return Task::Result::ContentType::Mbox;
    }

    return Task::Result::ContentType::None;
}

QString DecryptVerifyResult::overview() const
{
    QString ov;
    if (d->isDecryptOnly()) {
        ov += formatDecryptionResultOverview(d->m_decryptionResult);
    } else if (d->isVerifyOnly()) {
        ov += formatVerificationResultOverview(d->m_verificationResult, d->makeSenderInfo());
    } else {
        ov += formatDecryptVerifyResultOverview(d->m_decryptionResult, d->m_verificationResult, d->makeSenderInfo());
    }
    if (ov.size() + d->label().size() > 120) {
        // Avoid ugly breaks
        ov = QStringLiteral("<br>") + ov;
    }
    return i18nc("label: result example: foo.sig: Verification failed. ", "%1: %2", d->label(), ov);
}

QString DecryptVerifyResult::details() const
{
    if (d->isDecryptOnly()) {
        return formatDecryptionResultDetails(d->m_decryptionResult,
                                             KeyCache::instance()->findRecipients(d->m_decryptionResult),
                                             errorString(),
                                             false,
                                             parentTask());
    }
    if (d->isVerifyOnly()) {
        return formatVerificationResultDetails(d->m_verificationResult, d->makeSenderInfo(), errorString());
    }
    return formatDecryptVerifyResultDetails(d->m_decryptionResult,
                                            d->m_verificationResult,
                                            KeyCache::instance()->findRecipients(d->m_decryptionResult),
                                            d->makeSenderInfo(),
                                            errorString(),
                                            parentTask());
}

GpgME::Error DecryptVerifyResult::error() const
{
    return d->m_error;
}

QString DecryptVerifyResult::errorString() const
{
    return d->m_errorString;
}

AuditLogEntry DecryptVerifyResult::auditLog() const
{
    return d->m_auditLog;
}

Task::Result::VisualCode DecryptVerifyResult::code() const
{
    if ((d->m_type == DecryptVerify || d->m_type == Verify) && relevantInDecryptVerifyContext(verificationResult())) {
        return codeForVerificationResult(verificationResult());
    }
    return hasError() ? NeutralError : NeutralSuccess;
}

GpgME::VerificationResult DecryptVerifyResult::verificationResult() const
{
    return d->m_verificationResult;
}

GpgME::DecryptionResult DecryptVerifyResult::decryptionResult() const
{
    return d->m_decryptionResult;
}

QString DecryptVerifyResult::fileName() const
{
    return d->m_fileName;
}

class AbstractDecryptVerifyTask::Private
{
public:
    Mailbox informativeSender;
    QPointer<QGpgME::Job> job;
};

AbstractDecryptVerifyTask::AbstractDecryptVerifyTask(QObject *parent)
    : Task(parent)
    , d(new Private)
{
}

AbstractDecryptVerifyTask::~AbstractDecryptVerifyTask()
{
}

void AbstractDecryptVerifyTask::cancel()
{
    qCDebug(KLEOPATRA_LOG) << this << __func__;
    if (d->job) {
        d->job->slotCancel();
    }
}

Mailbox AbstractDecryptVerifyTask::informativeSender() const
{
    return d->informativeSender;
}

void AbstractDecryptVerifyTask::setInformativeSender(const Mailbox &sender)
{
    d->informativeSender = sender;
}

QGpgME::Job *AbstractDecryptVerifyTask::job() const
{
    return d->job;
}

void AbstractDecryptVerifyTask::setJob(QGpgME::Job *job)
{
    d->job = job;
}

class DecryptVerifyTask::Private
{
    DecryptVerifyTask *const q;

public:
    explicit Private(DecryptVerifyTask *qq)
        : q{qq}
    {
    }

    void startDecryptVerifyJob();
    void startDecryptVerifyArchiveJob();

    void slotResult(const DecryptionResult &, const VerificationResult &, const QByteArray & = {});

    std::shared_ptr<Input> m_input;
    std::shared_ptr<Output> m_output;
    const QGpgME::Protocol *m_backend = nullptr;
    Protocol m_protocol = UnknownProtocol;
    bool m_ignoreMDCError = false;
    bool m_extractArchive = false;
    QString m_inputFilePath;
    QString m_outputFilePath;
    QString m_outputDirectory;
};

void DecryptVerifyTask::Private::slotResult(const DecryptionResult &dr, const VerificationResult &vr, const QByteArray &plainText)
{
    updateKeys(vr);
    {
        std::stringstream ss;
        ss << dr << '\n' << vr;
        qCDebug(KLEOPATRA_LOG) << ss.str().c_str();
    }
    const AuditLogEntry auditLog = auditLogFromSender(q->sender());
    if (m_output) {
        if (dr.error().code() || vr.error().code()) {
            m_output->cancel();
        } else {
            try {
                kleo_assert(!dr.isNull() || !vr.isNull());
                m_output->finalize();
            } catch (const GpgME::Exception &e) {
                q->emitResult(q->fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog));
                return;
            } catch (const std::exception &e) {
                q->emitResult(
                    q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog));
                return;
            } catch (...) {
                q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog));
                return;
            }
        }
    }
    const int drErr = dr.error().code();
    const QString errorString = m_output ? m_output->errorString() : QString{};
    if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || (m_output && m_output->failed())) {
        q->emitResult(q->fromDecryptResult(drErr ? dr.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog));
        return;
    }

    q->emitResult(q->fromDecryptVerifyResult(dr, vr, plainText, m_output ? m_output->fileName() : m_outputFilePath, auditLog));
}

DecryptVerifyTask::DecryptVerifyTask(QObject *parent)
    : AbstractDecryptVerifyTask(parent)
    , d(new Private(this))
{
}

DecryptVerifyTask::~DecryptVerifyTask()
{
}

void DecryptVerifyTask::setInput(const std::shared_ptr<Input> &input)
{
    d->m_input = input;
    kleo_assert(d->m_input && d->m_input->ioDevice());
}

void DecryptVerifyTask::setOutput(const std::shared_ptr<Output> &output)
{
    d->m_output = output;
    kleo_assert(d->m_output && d->m_output->ioDevice());
}

void DecryptVerifyTask::setProtocol(Protocol prot)
{
    kleo_assert(prot != UnknownProtocol);
    d->m_protocol = prot;
    d->m_backend = prot == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime();
    kleo_assert(d->m_backend);
}

void DecryptVerifyTask::autodetectProtocolFromInput()
{
    if (!d->m_input) {
        return;
    }
    const Protocol p = findProtocol(d->m_input->classification());
    if (p == UnknownProtocol) {
        throw Exception(
            gpg_error(GPG_ERR_NOTHING_FOUND),
            i18n("Could not determine whether this is an S/MIME or an OpenPGP signature/ciphertext - maybe it is neither ciphertext nor a signature?"),
            Exception::MessageOnly);
    }
    setProtocol(p);
}

QString DecryptVerifyTask::label() const
{
    return i18n("Decrypting %1...", inputLabel());
}

unsigned long long DecryptVerifyTask::inputSize() const
{
    return d->m_input ? d->m_input->size() : 0;
}

QString DecryptVerifyTask::inputLabel() const
{
    return d->m_input ? d->m_input->label() : QFileInfo{d->m_inputFilePath}.fileName();
}

QString DecryptVerifyTask::outputLabel() const
{
    if (d->m_output) {
        return d->m_output->label();
    } else if (!d->m_outputFilePath.isEmpty()) {
        return QFileInfo{d->m_outputFilePath}.fileName();
    } else {
        return d->m_outputDirectory;
    }
}

Protocol DecryptVerifyTask::protocol() const
{
    return d->m_protocol;
}

static void ensureIOOpen(QIODevice *input, QIODevice *output)
{
    if (input && !input->isOpen()) {
        input->open(QIODevice::ReadOnly);
    }
    if (output && !output->isOpen()) {
        output->open(QIODevice::WriteOnly);
    }
}

void DecryptVerifyTask::setIgnoreMDCError(bool value)
{
    d->m_ignoreMDCError = value;
}

void DecryptVerifyTask::setExtractArchive(bool extract)
{
    d->m_extractArchive = extract;
}

void DecryptVerifyTask::setInputFile(const QString &path)
{
    d->m_inputFilePath = path;
}

void DecryptVerifyTask::setOutputFile(const QString &path)
{
    d->m_outputFilePath = path;
}

void DecryptVerifyTask::setOutputDirectory(const QString &directory)
{
    d->m_outputDirectory = directory;
}

static bool archiveJobsCanBeUsed(GpgME::Protocol protocol)
{
    return (protocol == GpgME::OpenPGP) && QGpgME::DecryptVerifyArchiveJob::isSupported();
}

void DecryptVerifyTask::doStart()
{
    kleo_assert(d->m_backend);
    if (d->m_extractArchive && archiveJobsCanBeUsed(d->m_protocol)) {
        d->startDecryptVerifyArchiveJob();
    } else {
        d->startDecryptVerifyJob();
    }
}

static void setIgnoreMDCErrorFlag(QGpgME::Job *job, bool ignoreMDCError)
{
    if (ignoreMDCError) {
        qCDebug(KLEOPATRA_LOG) << "Modifying job to ignore MDC errors.";
        auto ctx = QGpgME::Job::context(job);
        if (!ctx) {
            qCWarning(KLEOPATRA_LOG) << "Failed to get context for job";
        } else {
            const auto err = ctx->setFlag("ignore-mdc-error", "1");
            if (err) {
                qCWarning(KLEOPATRA_LOG) << "Failed to set ignore mdc errors" << Formatting::errorAsString(err);
            }
        }
    }
}

void DecryptVerifyTask::Private::startDecryptVerifyJob()
{
#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO
    if (!m_outputFilePath.isEmpty() && QFile::exists(m_outputFilePath)) {
        // The output files are always written to a temporary location. Therefore, this can only occur
        // if two signed/encrypted files with the same name in different folders are verified/decrypted
        // because they would be written to the same temporary location.
        QMetaObject::invokeMethod(
            q,
            [this]() {
                slotResult(DecryptionResult{Error::fromCode(GPG_ERR_EEXIST)}, VerificationResult{});
            },
            Qt::QueuedConnection);
        return;
    }
#endif
    try {
        std::unique_ptr<QGpgME::DecryptVerifyJob> job{m_backend->decryptVerifyJob()};
        kleo_assert(job);
        setIgnoreMDCErrorFlag(job.get(), m_ignoreMDCError);
        QObject::connect(job.get(),
                         &QGpgME::DecryptVerifyJob::result,
                         q,
                         [this](const GpgME::DecryptionResult &decryptResult, const GpgME::VerificationResult &verifyResult, const QByteArray &plainText) {
                             slotResult(decryptResult, verifyResult, plainText);
                         });
        connect(job.get(), &QGpgME::Job::jobProgress, q, &DecryptVerifyTask::setProgress);
#if QGPGME_SUPPORTS_PROCESS_ALL_SIGNATURES
        job->setProcessAllSignatures(true);
#endif
#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO
        if (!m_inputFilePath.isEmpty() && !m_outputFilePath.isEmpty()) {
            job->setInputFile(m_inputFilePath);
            job->setOutputFile(m_outputFilePath);
            const auto err = job->startIt();
        } else {
            ensureIOOpen(m_input->ioDevice().get(), m_output->ioDevice().get());
            job->start(m_input->ioDevice(), m_output->ioDevice());
        }
#else
        ensureIOOpen(m_input->ioDevice().get(), m_output->ioDevice().get());
        job->start(m_input->ioDevice(), m_output->ioDevice());
#endif
        q->setJob(job.release());
    } catch (const GpgME::Exception &e) {
        q->emitResult(q->fromDecryptVerifyResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry()));
    } catch (const std::exception &e) {
        q->emitResult(
            q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry()));
    } catch (...) {
        q->emitResult(q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry()));
    }
}

void DecryptVerifyTask::Private::startDecryptVerifyArchiveJob()
{
    std::unique_ptr<QGpgME::DecryptVerifyArchiveJob> job{m_backend->decryptVerifyArchiveJob()};
    kleo_assert(job);
    setIgnoreMDCErrorFlag(job.get(), m_ignoreMDCError);
    connect(job.get(),
            &QGpgME::DecryptVerifyArchiveJob::result,
            q,
            [this](const GpgME::DecryptionResult &decryptResult, const GpgME::VerificationResult &verifyResult) {
                slotResult(decryptResult, verifyResult);
            });
    connect(job.get(), &QGpgME::Job::jobProgress, q, &DecryptVerifyTask::setProgress);
    // make sure that we don't use an existing output directory
    const auto outputDirectory = ensureUniqueDirectory(m_outputDirectory);
    if (outputDirectory.isEmpty()) {
        q->emitResult(q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_GENERAL), {}, {}));
        return;
    }
    m_outputDirectory = outputDirectory;
#if QGPGME_SUPPORTS_PROCESS_ALL_SIGNATURES
    job->setProcessAllSignatures(true);
#endif
    job->setInputFile(m_inputFilePath);
    job->setOutputDirectory(m_outputDirectory);
    const auto err = job->startIt();
    q->setJob(job.release());
    if (err) {
        q->emitResult(q->fromDecryptVerifyResult(err, {}, {}));
    }
}

class DecryptTask::Private
{
    DecryptTask *const q;

public:
    explicit Private(DecryptTask *qq)
        : q{qq}
    {
    }

    void slotResult(const DecryptionResult &, const QByteArray &);

    void registerJob(QGpgME::DecryptJob *job)
    {
        q->connect(job, SIGNAL(result(GpgME::DecryptionResult, QByteArray)), q, SLOT(slotResult(GpgME::DecryptionResult, QByteArray)));
        q->connect(job, &QGpgME::Job::jobProgress, q, &DecryptTask::setProgress);
    }

    std::shared_ptr<Input> m_input;
    std::shared_ptr<Output> m_output;
    const QGpgME::Protocol *m_backend = nullptr;
    Protocol m_protocol = UnknownProtocol;
};

void DecryptTask::Private::slotResult(const DecryptionResult &result, const QByteArray &plainText)
{
    {
        std::stringstream ss;
        ss << result;
        qCDebug(KLEOPATRA_LOG) << ss.str().c_str();
    }
    const AuditLogEntry auditLog = auditLogFromSender(q->sender());
    if (result.error().code()) {
        m_output->cancel();
    } else {
        try {
            kleo_assert(!result.isNull());
            m_output->finalize();
        } catch (const GpgME::Exception &e) {
            q->emitResult(q->fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog));
            return;
        } catch (const std::exception &e) {
            q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog));
            return;
        } catch (...) {
            q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog));
            return;
        }
    }

    const int drErr = result.error().code();
    const QString errorString = m_output->errorString();
    if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || m_output->failed()) {
        q->emitResult(q->fromDecryptResult(result.error() ? result.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog));
        return;
    }

    q->emitResult(q->fromDecryptResult(result, plainText, auditLog));
}

DecryptTask::DecryptTask(QObject *parent)
    : AbstractDecryptVerifyTask(parent)
    , d(new Private(this))
{
}

DecryptTask::~DecryptTask()
{
}

void DecryptTask::setInput(const std::shared_ptr<Input> &input)
{
    d->m_input = input;
    kleo_assert(d->m_input && d->m_input->ioDevice());
}

void DecryptTask::setOutput(const std::shared_ptr<Output> &output)
{
    d->m_output = output;
    kleo_assert(d->m_output && d->m_output->ioDevice());
}

void DecryptTask::setProtocol(Protocol prot)
{
    kleo_assert(prot != UnknownProtocol);
    d->m_protocol = prot;
    d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
    kleo_assert(d->m_backend);
}

void DecryptTask::autodetectProtocolFromInput()
{
    if (!d->m_input) {
        return;
    }
    const Protocol p = findProtocol(d->m_input->classification());
    if (p == UnknownProtocol) {
        throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND),
                        i18n("Could not determine whether this was S/MIME- or OpenPGP-encrypted - maybe it is not ciphertext at all?"),
                        Exception::MessageOnly);
    }
    setProtocol(p);
}

QString DecryptTask::label() const
{
    return i18n("Decrypting: %1...", d->m_input->label());
}

unsigned long long DecryptTask::inputSize() const
{
    return d->m_input ? d->m_input->size() : 0;
}

QString DecryptTask::inputLabel() const
{
    return d->m_input ? d->m_input->label() : QString();
}

QString DecryptTask::outputLabel() const
{
    return d->m_output ? d->m_output->label() : QString();
}

Protocol DecryptTask::protocol() const
{
    return d->m_protocol;
}

void DecryptTask::doStart()
{
    kleo_assert(d->m_backend);

    try {
        std::unique_ptr<QGpgME::DecryptJob> job{d->m_backend->decryptJob()};
        kleo_assert(job);
        d->registerJob(job.get());
        ensureIOOpen(d->m_input->ioDevice().get(), d->m_output->ioDevice().get());
        job->start(d->m_input->ioDevice(), d->m_output->ioDevice());
        setJob(job.release());
    } catch (const GpgME::Exception &e) {
        emitResult(fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry()));
    } catch (const std::exception &e) {
        emitResult(fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry()));
    } catch (...) {
        emitResult(fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry()));
    }
}

class VerifyOpaqueTask::Private
{
    VerifyOpaqueTask *const q;

public:
    explicit Private(VerifyOpaqueTask *qq)
        : q{qq}
    {
    }

    void startVerifyOpaqueJob();
    void startDecryptVerifyArchiveJob();

    void slotResult(const VerificationResult &, const QByteArray & = {});

    std::shared_ptr<Input> m_input;
    std::shared_ptr<Output> m_output;
    const QGpgME::Protocol *m_backend = nullptr;
    Protocol m_protocol = UnknownProtocol;
    bool m_extractArchive = false;
    QString m_inputFilePath;
    QString m_outputFilePath;
    QString m_outputDirectory;
};

void VerifyOpaqueTask::Private::slotResult(const VerificationResult &result, const QByteArray &plainText)
{
    updateKeys(result);
    {
        std::stringstream ss;
        ss << result;
        qCDebug(KLEOPATRA_LOG) << ss.str().c_str();
    }
    const AuditLogEntry auditLog = auditLogFromSender(q->sender());
    if (m_output) {
        if (result.error().code()) {
            m_output->cancel();
        } else {
            try {
                kleo_assert(!result.isNull());
                m_output->finalize();
            } catch (const GpgME::Exception &e) {
                q->emitResult(q->fromVerifyOpaqueResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog));
                return;
            } catch (const std::exception &e) {
                q->emitResult(
                    q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog));
                return;
            } catch (...) {
                q->emitResult(q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog));
                return;
            }
        }
    }

    const int drErr = result.error().code();
    const QString errorString = m_output ? m_output->errorString() : QString{};
    if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || (m_output && m_output->failed())) {
        q->emitResult(q->fromVerifyOpaqueResult(result.error() ? result.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog));
        return;
    }

    q->emitResult(q->fromVerifyOpaqueResult(result, plainText, auditLog));
}

VerifyOpaqueTask::VerifyOpaqueTask(QObject *parent)
    : AbstractDecryptVerifyTask(parent)
    , d(new Private(this))
{
}

VerifyOpaqueTask::~VerifyOpaqueTask()
{
}

void VerifyOpaqueTask::setInput(const std::shared_ptr<Input> &input)
{
    d->m_input = input;
    kleo_assert(d->m_input && d->m_input->ioDevice());
}

void VerifyOpaqueTask::setOutput(const std::shared_ptr<Output> &output)
{
    d->m_output = output;
    kleo_assert(d->m_output && d->m_output->ioDevice());
}

void VerifyOpaqueTask::setProtocol(Protocol prot)
{
    kleo_assert(prot != UnknownProtocol);
    d->m_protocol = prot;
    d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
    kleo_assert(d->m_backend);
}

void VerifyOpaqueTask::autodetectProtocolFromInput()
{
    if (!d->m_input) {
        return;
    }
    const Protocol p = findProtocol(d->m_input->classification());
    if (p == UnknownProtocol) {
        throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND),
                        i18n("Could not determine whether this is an S/MIME or an OpenPGP signature - maybe it is not a signature at all?"),
                        Exception::MessageOnly);
    }
    setProtocol(p);
}

QString VerifyOpaqueTask::label() const
{
    return i18n("Verifying %1...", inputLabel());
}

unsigned long long VerifyOpaqueTask::inputSize() const
{
    return d->m_input ? d->m_input->size() : 0;
}

QString VerifyOpaqueTask::inputLabel() const
{
    return d->m_input ? d->m_input->label() : QFileInfo{d->m_inputFilePath}.fileName();
}

QString VerifyOpaqueTask::outputLabel() const
{
    if (d->m_output) {
        return d->m_output->label();
    } else if (!d->m_outputFilePath.isEmpty()) {
        return QFileInfo{d->m_outputFilePath}.fileName();
    } else {
        return d->m_outputDirectory;
    }
}

Protocol VerifyOpaqueTask::protocol() const
{
    return d->m_protocol;
}

void VerifyOpaqueTask::setExtractArchive(bool extract)
{
    d->m_extractArchive = extract;
}

void VerifyOpaqueTask::setInputFile(const QString &path)
{
    d->m_inputFilePath = path;
}

void VerifyOpaqueTask::setOutputFile(const QString &path)
{
    d->m_outputFilePath = path;
}

void VerifyOpaqueTask::setOutputDirectory(const QString &directory)
{
    d->m_outputDirectory = directory;
}

void VerifyOpaqueTask::doStart()
{
    kleo_assert(d->m_backend);
    if (d->m_extractArchive && archiveJobsCanBeUsed(d->m_protocol)) {
        d->startDecryptVerifyArchiveJob();
    } else {
        d->startVerifyOpaqueJob();
    }
}

void VerifyOpaqueTask::Private::startVerifyOpaqueJob()
{
#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO
    if (!m_outputFilePath.isEmpty() && QFile::exists(m_outputFilePath)) {
        // The output files are always written to a temporary location. Therefore, this can only occur
        // if two signed/encrypted files with the same name in different folders are verified/decrypted
        // because they would be written to the same temporary location.
        QMetaObject::invokeMethod(
            q,
            [this]() {
                slotResult(VerificationResult{Error::fromCode(GPG_ERR_EEXIST)});
            },
            Qt::QueuedConnection);
        return;
    }
#endif
    try {
        std::unique_ptr<QGpgME::VerifyOpaqueJob> job{m_backend->verifyOpaqueJob()};
        kleo_assert(job);
        connect(job.get(), &QGpgME::VerifyOpaqueJob::result, q, [this](const GpgME::VerificationResult &result, const QByteArray &plainText) {
            slotResult(result, plainText);
        });
        connect(job.get(), &QGpgME::Job::jobProgress, q, &VerifyOpaqueTask::setProgress);
#if QGPGME_SUPPORTS_PROCESS_ALL_SIGNATURES
        job->setProcessAllSignatures(true);
#endif
#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO
        if (!m_inputFilePath.isEmpty() && !m_outputFilePath.isEmpty()) {
            job->setInputFile(m_inputFilePath);
            job->setOutputFile(m_outputFilePath);
            const auto err = job->startIt();
        } else {
            ensureIOOpen(m_input->ioDevice().get(), m_output ? m_output->ioDevice().get() : nullptr);
            job->start(m_input->ioDevice(), m_output ? m_output->ioDevice() : std::shared_ptr<QIODevice>());
        }
#else
        ensureIOOpen(m_input->ioDevice().get(), m_output ? m_output->ioDevice().get() : nullptr);
        job->start(m_input->ioDevice(), m_output ? m_output->ioDevice() : std::shared_ptr<QIODevice>());
#endif
        q->setJob(job.release());
    } catch (const GpgME::Exception &e) {
        q->emitResult(q->fromVerifyOpaqueResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry()));
    } catch (const std::exception &e) {
        q->emitResult(
            q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry()));
    } catch (...) {
        q->emitResult(q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry()));
    }
}

void VerifyOpaqueTask::Private::startDecryptVerifyArchiveJob()
{
    std::unique_ptr<QGpgME::DecryptVerifyArchiveJob> job{m_backend->decryptVerifyArchiveJob()};
    kleo_assert(job);
    connect(job.get(), &QGpgME::DecryptVerifyArchiveJob::result, q, [this](const DecryptionResult &, const VerificationResult &verifyResult) {
        slotResult(verifyResult);
    });
    connect(job.get(), &QGpgME::DecryptVerifyArchiveJob::dataProgress, q, &VerifyOpaqueTask::setProgress);
    // make sure that we don't use an existing output directory
    const auto outputDirectory = ensureUniqueDirectory(m_outputDirectory);
    if (outputDirectory.isEmpty()) {
        q->emitResult(q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_GENERAL), {}, {}));
        return;
    }
    m_outputDirectory = outputDirectory;
#if QGPGME_SUPPORTS_PROCESS_ALL_SIGNATURES
    job->setProcessAllSignatures(true);
#endif
    job->setInputFile(m_inputFilePath);
    job->setOutputDirectory(m_outputDirectory);
    const auto err = job->startIt();
    q->setJob(job.release());
    if (err) {
        q->emitResult(q->fromVerifyOpaqueResult(err, {}, {}));
    }
}

class VerifyDetachedTask::Private
{
    VerifyDetachedTask *const q;

public:
    explicit Private(VerifyDetachedTask *qq)
        : q{qq}
    {
    }

    void slotResult(const VerificationResult &);

    void registerJob(QGpgME::VerifyDetachedJob *job)
    {
        q->connect(job, SIGNAL(result(GpgME::VerificationResult)), q, SLOT(slotResult(GpgME::VerificationResult)));
        q->connect(job, &QGpgME::Job::jobProgress, q, &VerifyDetachedTask::setProgress);
    }

    QString signatureLabel() const;
    QString signedDataLabel() const;

    std::shared_ptr<Input> m_input, m_signedData;
    const QGpgME::Protocol *m_backend = nullptr;
    Protocol m_protocol = UnknownProtocol;
    QString m_signatureFilePath;
    QString m_signedFilePath;
};

void VerifyDetachedTask::Private::slotResult(const VerificationResult &result)
{
    updateKeys(result);
    {
        std::stringstream ss;
        ss << result;
        qCDebug(KLEOPATRA_LOG) << ss.str().c_str();
    }
    const AuditLogEntry auditLog = auditLogFromSender(q->sender());
    try {
        kleo_assert(!result.isNull());
        q->emitResult(q->fromVerifyDetachedResult(result, auditLog));
    } catch (const GpgME::Exception &e) {
        q->emitResult(q->fromVerifyDetachedResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog));
    } catch (const std::exception &e) {
        q->emitResult(q->fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog));
    } catch (...) {
        q->emitResult(q->fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog));
    }
}

QString VerifyDetachedTask::Private::signatureLabel() const
{
    return m_input ? m_input->label() : m_signatureFilePath;
}

QString VerifyDetachedTask::Private::signedDataLabel() const
{
    return m_signedData ? m_signedData->label() : m_signedFilePath;
}

VerifyDetachedTask::VerifyDetachedTask(QObject *parent)
    : AbstractDecryptVerifyTask(parent)
    , d(new Private(this))
{
}

VerifyDetachedTask::~VerifyDetachedTask()
{
}

void VerifyDetachedTask::setInput(const std::shared_ptr<Input> &input)
{
    d->m_input = input;
    kleo_assert(d->m_input && d->m_input->ioDevice());
}

void VerifyDetachedTask::setSignedData(const std::shared_ptr<Input> &signedData)
{
    d->m_signedData = signedData;
    kleo_assert(d->m_signedData && d->m_signedData->ioDevice());
}

void VerifyDetachedTask::setSignatureFile(const QString &path)
{
    d->m_signatureFilePath = path;
}

void VerifyDetachedTask::setSignedFile(const QString &path)
{
    d->m_signedFilePath = path;
}

void VerifyDetachedTask::setProtocol(Protocol prot)
{
    kleo_assert(prot != UnknownProtocol);
    d->m_protocol = prot;
    d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
    kleo_assert(d->m_backend);
}

void VerifyDetachedTask::autodetectProtocolFromInput()
{
    if (!d->m_input) {
        return;
    }
    const Protocol p = findProtocol(d->m_input->classification());
    if (p == UnknownProtocol) {
        throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND),
                        i18n("Could not determine whether this is an S/MIME or an OpenPGP signature - maybe it is not a signature at all?"),
                        Exception::MessageOnly);
    }
    setProtocol(p);
}

unsigned long long VerifyDetachedTask::inputSize() const
{
    return d->m_signedData ? d->m_signedData->size() : 0;
}

QString VerifyDetachedTask::label() const
{
    const QString signedDataLabel = d->signedDataLabel();
    if (!signedDataLabel.isEmpty()) {
        return xi18nc(
            "Verification of a detached signature in progress. The first file contains the data."
            "The second file is the signature file.",
            "Verifying <filename>%1</filename> with <filename>%2</filename>...",
            signedDataLabel,
            d->signatureLabel());
    }
    return i18n("Verifying signature %1...", d->signatureLabel());
}

QString VerifyDetachedTask::inputLabel() const
{
    const QString signatureLabel = d->signatureLabel();
    const QString signedDataLabel = d->signedDataLabel();
    if (!signedDataLabel.isEmpty() && !signatureLabel.isEmpty()) {
        return xi18nc(
            "Verification of a detached signature summary. The first file contains the data."
            "The second file is signature.",
            "Verified <filename>%1</filename> with <filename>%2</filename>",
            signedDataLabel,
            signatureLabel);
    }
    return signatureLabel;
}

QString VerifyDetachedTask::outputLabel() const
{
    return QString();
}

Protocol VerifyDetachedTask::protocol() const
{
    return d->m_protocol;
}

void VerifyDetachedTask::doStart()
{
    kleo_assert(d->m_backend);
    try {
        std::unique_ptr<QGpgME::VerifyDetachedJob> job{d->m_backend->verifyDetachedJob()};
        kleo_assert(job);
        d->registerJob(job.get());
#if QGPGME_SUPPORTS_PROCESS_ALL_SIGNATURES
        job->setProcessAllSignatures(true);
#endif
#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO
        if (d->m_protocol == GpgME::OpenPGP && !d->m_signatureFilePath.isEmpty() && !d->m_signedFilePath.isEmpty()) {
            job->setSignatureFile(d->m_signatureFilePath);
            job->setSignedFile(d->m_signedFilePath);
            job->startIt();
        } else {
            ensureIOOpen(d->m_input->ioDevice().get(), nullptr);
            ensureIOOpen(d->m_signedData->ioDevice().get(), nullptr);
            job->start(d->m_input->ioDevice(), d->m_signedData->ioDevice());
        }
#else
        ensureIOOpen(d->m_input->ioDevice().get(), nullptr);
        ensureIOOpen(d->m_signedData->ioDevice().get(), nullptr);
        job->start(d->m_input->ioDevice(), d->m_signedData->ioDevice());
#endif
        setJob(job.release());
    } catch (const GpgME::Exception &e) {
        emitResult(fromVerifyDetachedResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry()));
    } catch (const std::exception &e) {
        emitResult(
            fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry()));
    } catch (...) {
        emitResult(fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry()));
    }
}

#include "moc_decryptverifytask.cpp"
