/*
 * Copyright (C) 2014-2022 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <cinttypes>
#include <cstdlib> /* ::std::free */
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFont>
#include <QIcon>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QList>
#include <QMap>
#include <QModelIndex>
#include <QMutexLocker>
#include <QPair>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QString>
#include <QStringBuilder>
#include <QTimeZone>
#include <QVariant>
#include <QVector>

#include "src/crypto/crypto_funcs.h"
#include "src/datovka_shared/compat_qt/misc.h" /* _Qt_SkipEmptyParts */
#include "src/datovka_shared/compat_qt/variant.h" /* nullVariantWhenIsNull */
#include "src/datovka_shared/isds/type_conversion.h"
#include "src/datovka_shared/log/log.h"
#include "src/global.h"
#include "src/io/dbs.h"
#include "src/io/message_db.h"
#include "src/io/message_db_tables.h"
#include "src/isds/type_description.h"
#include "src/settings/prefs_specific.h"

#define INVALID_YEAR "inv"
const QString MessageDb::invalidYearName(INVALID_YEAR);

/* Attachment size is computed from actual data. */
static
const QVector<QString> fileItemIdsNoSize = {"id", "dmEncodedContent",
    "_dmFileDescr", "_dmMimeType"};
static
const QVector<QString> fileItemIdsNoDataSize = {"id", "null",
    "_dmFileDescr", "_dmMimeType"};

const QVector<QString> MessageDb::msgPrintedAttribs = {"dmSenderIdent",
    "dmSenderRefNumber", "dmRecipientIdent", "dmRecipientRefNumber",
    "dmToHands", "dmLegalTitleLaw", "dmLegalTitleYear", "dmLegalTitleSect",
    "dmLegalTitlePar", "dmLegalTitlePoint"};

const QVector<QString> MessageDb::msgDeliveryBoolAttribs = {"dmPersonalDelivery",
    "dmAllowSubstDelivery"};

const QVector<QString> MessageDb::msgStatus = {"dmDeliveryTime",
    "dmAcceptanceTime", "dmMessageStatus"};

const QVector<QString> MessageDb::rcvdItemIds = {"dmID", "dmPersonalDelivery",
    "dmAnnotation", "dmSender", "dmDeliveryTime", "dmAcceptanceTime",
    "read_locally", "dmMessageStatus", "is_downloaded", "process_status"};

const QVector<QString> MessageDb::sntItemIds = {"dmID", "dmAnnotation",
    "dmRecipient", "dmDeliveryTime", "dmAcceptanceTime", "dmMessageStatus",
    "is_downloaded"};

const QVector<QString> MessageDb::fileItemIds = {"id", "message_id",
    "dmEncodedContent", "_dmFileDescr", "_dmMimeType",
    "LENGTH(dmEncodedContent)"};

/*!
 * @brief Join vector elements using separator.
 *
 * @param[in] vect Vector.
 * @param[in] sep Separator.
 * @return Joined string.
 */
static
QString joinStrVect(const QVector<QString> &vect, const QString &sep)
{
	QString str;
	if (vect.size() > 0) {
		for (qsizetype i = 0; i < (vect.size() - 1); ++i) {
			str += vect[i] + sep;
		}
		str += vect.last();
	}
	return str;
}

void MessageDb::declareTypes(void)
{
	qRegisterMetaType<MessageDb::MessageProcessState>("MessageDb::MessageProcessState");
}

MessageDb::MessageDb(const QString &connectionName)
    : DelayedAccessSQLiteDb(connectionName)
{
}

void MessageDb::appendRcvdEntryList(QList<RcvdEntry> &entryList,
    QSqlQuery &query)
{
	query.first();
	while (query.isActive() && query.isValid()) {
		entryList.append(MessageDb::RcvdEntry(
		    query.value(0).toLongLong(),
		    query.value(1).toBool(), query.value(2).toString(),
		    query.value(3).toString(), query.value(4).toString(),
		    query.value(5).toString(), query.value(6).toBool(),
		    Isds::variant2DmState(query.value(7)),
		    query.value(8).toBool(), query.value(9).toInt()));
		query.next();
	}
}

QList<MessageDb::RcvdEntry> MessageDb::msgsRcvdEntries(void)
{
	QMutexLocker locker(&m_lock);

	QList<RcvdEntry> entryList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT ";
	for (int i = 0; i < (rcvdItemIds.size() - 2); ++i) {
		queryStr += rcvdItemIds[i] + ", ";
	}
	queryStr += "(ifnull(r.message_id, 0) != 0) AS is_downloaded" ", "
	    "ifnull(p.state, 0) AS process_status";
	queryStr += " FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "LEFT JOIN raw_message_data AS r "
	    "ON (m.dmId = r.message_id) "
	    "LEFT JOIN process_state AS p "
	    "ON (m.dmId = p.message_id) "
	    "WHERE s.message_type = :message_type";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", TYPE_RECEIVED);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	appendRcvdEntryList(entryList, query);
	return entryList;
fail:
	return QList<RcvdEntry>();
}

QList<MessageDb::RcvdEntry> MessageDb::msgsRcvdEntriesWithin90Days(void)
{
	QMutexLocker locker(&m_lock);

	QList<RcvdEntry> entryList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	if (Q_UNLIKELY(!msgsRcvdWithin90DaysQuery(query))) {
		goto fail;
	}

	appendRcvdEntryList(entryList, query);
	return entryList;
fail:
	return QList<RcvdEntry>();
}

QList<MessageDb::RcvdEntry> MessageDb::msgsRcvdEntriesInYear(
    const QString &year)
{
	QMutexLocker locker(&m_lock);

	QList<RcvdEntry> entryList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT ";
	for (int i = 0; i < (rcvdItemIds.size() - 2); ++i) {
		queryStr += rcvdItemIds[i] + ", ";
	}
	queryStr += "(ifnull(r.message_id, 0) != 0) AS is_downloaded" ", "
	    "ifnull(p.state, 0) AS process_status";
	queryStr += " FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "LEFT JOIN raw_message_data AS r "
	    "ON (m.dmId = r.message_id) "
	    "LEFT JOIN process_state AS p "
	    "ON (m.dmId = p.message_id) "
	    "WHERE (s.message_type = :message_type) and "
	    "(ifnull(strftime('%Y', m.dmDeliveryTime), "
	    "'" INVALID_YEAR "') = :year)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", TYPE_RECEIVED);
	query.bindValue(":year", year);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	appendRcvdEntryList(entryList, query);
	return entryList;
fail:
	return QList<RcvdEntry>();
}

QStringList MessageDb::msgsYears(enum MessageDb::MessageType type,
    enum Sorting sorting)
{
	QMutexLocker locker(&m_lock);

	QStringList yearList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT DISTINCT "
	    "ifnull(strftime('%Y', m.dmDeliveryTime), '" INVALID_YEAR "') "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE s.message_type = :message_type";

	switch (sorting) {
	case ASCENDING:
		queryStr += " ORDER BY dmDeliveryTime ASC";
		break;
	case DESCENDING:
		queryStr += " ORDER BY dmDeliveryTime DESC";
		break;
	default:
		break;
	}

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", type);
	if (query.exec()) {
		query.first();
		while (query.isValid()) {
			yearList.append(query.value(0).toString());
			query.next();
		}
		return yearList;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QStringList();
}

QList< QPair<QString, int> > MessageDb::msgsYearlyCounts(enum MessageType type,
    enum Sorting sorting)
{
	QMutexLocker locker(&m_lock);

	QList< QPair<QString, int> > yearlyCounts;
	QList<QString> yearList = msgsYears(type, sorting);
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;

	for (int i = 0; i < yearList.size(); ++i) {
		queryStr = "SELECT COUNT(*) AS nrRecords "
		    "FROM messages AS m "
		    "LEFT JOIN supplementary_message_data AS s "
		    "ON (m.dmID = s.message_id) "
		    "WHERE "
		    "(s.message_type = :message_type)"
		    " and "
		    "(ifnull(strftime('%Y', m.dmDeliveryTime), "
		    "'" INVALID_YEAR "') = :year)";

		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_type", type);
		query.bindValue(":year", yearList[i]);
		if (query.exec() && query.isActive() &&
		    query.first() && query.isValid()) {
			yearlyCounts.append(QPair<QString, int>(yearList[i],
			    query.value(0).toInt()));
		} else {
			logErrorNL(
			    "Cannot execute SQL query and/or read SQL data: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
	}
	return yearlyCounts;
fail:
	yearlyCounts.clear();
	return yearlyCounts;
}

int MessageDb::msgsUnreadWithin90Days(enum MessageType type)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT COUNT(*) AS nrUnread "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE (s.message_type = :message_type) and "
	    "(m.dmDeliveryTime >= date('now','-90 day')) and "
	    "(read_locally = 0)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", type);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toInt();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return -1;
}

int MessageDb::msgsUnreadInYear(enum MessageType type, const QString &year)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT COUNT(*) AS nrUnread "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE (s.message_type = :message_type) and "
	    "(ifnull(strftime('%Y', m.dmDeliveryTime), "
	    "'" INVALID_YEAR "') = :year) and (read_locally = 0)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", type);
	query.bindValue(":year", year);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toInt();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return -1;
}

int MessageDb::msgsUnaccepted(enum MessageType type)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT COUNT(*) as nrUnaccepted "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE (s.message_type = :message_type) and (dmMessageStatus < :messageStatus)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", type);
	query.bindValue(":messageStatus", Isds::Type::MS_ACCEPTED_FICT);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toInt();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return -1;
}

void MessageDb::appendSntEntryList(QList<SntEntry> &entryList, QSqlQuery &query)
{
	query.first();
	while (query.isActive() && query.isValid()) {
		entryList.append(SntEntry(query.value(0).toLongLong(),
		    query.value(1).toString(), query.value(2).toString(),
		    query.value(3).toString(), query.value(4).toString(),
		    Isds::variant2DmState(query.value(5)),
		    query.value(6).toBool()));
		query.next();
	}
}

QList<MessageDb::SntEntry> MessageDb::msgsSntEntries(void)
{
	QMutexLocker locker(&m_lock);

	QList<SntEntry> entryList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT ";
	for (int i = 0; i < (sntItemIds.size() - 1); ++i) {
		queryStr += sntItemIds[i] + ", ";
	}
	queryStr += "(ifnull(r.message_id, 0) != 0) AS is_downloaded";
	queryStr += " FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "LEFT JOIN raw_message_data AS r "
	    "ON (m.dmId = r.message_id) "
	    "WHERE s.message_type = :message_type";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", TYPE_SENT);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	appendSntEntryList(entryList, query);
	return entryList;
fail:
	return QList<SntEntry>();
}

QList<MessageDb::SntEntry> MessageDb::msgsSntEntriesWithin90Days(void)
{
	QMutexLocker locker(&m_lock);

	QList<SntEntry> entryList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	if (Q_UNLIKELY(!msgsSntWithin90DaysQuery(query))) {
		goto fail;
	}

	appendSntEntryList(entryList, query);
	return entryList;
fail:
	return QList<SntEntry>();
}

QList<MessageDb::SntEntry> MessageDb::msgsSntEntriesInYear(const QString &year)
{
	QMutexLocker locker(&m_lock);

	QList<SntEntry> entryList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT ";
	for (int i = 0; i < (sntItemIds.size() - 1); ++i) {
		queryStr += sntItemIds[i] + ", ";
	}
	queryStr += "(ifnull(r.message_id, 0) != 0) AS is_downloaded";
	queryStr += " FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "LEFT JOIN raw_message_data AS r "
	    "ON (m.dmId = r.message_id) "
	    "WHERE (s.message_type = :message_type) "
	    "and (ifnull(strftime('%Y', m.dmDeliveryTime), "
	    "'" INVALID_YEAR "') = :year)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", TYPE_SENT);
	query.bindValue(":year", year);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	appendSntEntryList(entryList, query);
	return entryList;
fail:
	return QList<SntEntry>();
}

Isds::Envelope MessageDb::getMessageEnvelope(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	Isds::Envelope envData;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT dbIDSender, dmSender, dmSenderAddress, "
	    "dmSenderType, dbIDRecipient, dmRecipient, dmRecipientAddress, "
	    "dmAnnotation, dmSenderRefNumber, dmSenderIdent, "
	    "dmRecipientRefNumber, dmRecipientIdent, "
	    "dmToHands, dmPersonalDelivery, dmAllowSubstDelivery, "
	    "dmLegalTitleLaw, dmLegalTitleYear, dmLegalTitleSect, "
	    "dmLegalTitlePar, dmLegalTitlePoint, _dmType "
	    "FROM messages WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		envData.setDbIDSender(query.value(0).toString());
		envData.setDmSender(query.value(1).toString());
		envData.setDmSenderAddress(query.value(2).toString());
		envData.setDmSenderType(Isds::intVariant2DbType(query.value(3)));
		envData.setDbIDRecipient(query.value(4).toString());
		envData.setDmRecipient(query.value(5).toString());
		envData.setDmRecipientAddress(query.value(6).toString());
		envData.setDmAnnotation(query.value(7).toString());
		envData.setDmSenderRefNumber(query.value(8).toString());
		envData.setDmSenderIdent(query.value(9).toString());
		envData.setDmRecipientRefNumber(query.value(10).toString());
		envData.setDmRecipientIdent(query.value(11).toString());
		envData.setDmToHands(query.value(12).toString());
		envData.setDmPersonalDelivery(Isds::variant2NilBool(query.value(13)));
		envData.setDmAllowSubstDelivery(Isds::variant2NilBool(query.value(14)));
		envData.setDmLegalTitleLawStr(query.value(15).toString());
		envData.setDmLegalTitleYearStr(query.value(16).toString());
		envData.setDmLegalTitleSect(query.value(17).toString());
		envData.setDmLegalTitlePar(query.value(18).toString());
		envData.setDmLegalTitlePoint(query.value(19).toString());
		envData.setDmType(Isds::variant2Char(query.value(20)));
		return envData;
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return Isds::Envelope();
}

int MessageDb::getMessageType(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT message_type "
	    "FROM supplementary_message_data WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toInt();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return -1;
}

enum MessageDb::MsgVerificationResult MessageDb::isMessageVerified(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT is_verified FROM messages "
	    "WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		/* If no value is set then the conversion will fail. */
		if (Q_UNLIKELY(query.value(0).isNull())) {
			goto fail;
		} else {
			return (query.value(0).toBool()) ? MSG_SIG_OK
			    : MSG_SIG_BAD;
		}
	} else {
		logErrorNL("Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return MSG_NOT_PRESENT;
}

bool MessageDb::messageLocallyRead(qint64 dmId, bool *ok)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr = "SELECT read_locally "
	    "FROM supplementary_message_data WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		if (ok != Q_NULLPTR) {
			*ok = true;
		}
		return query.value(0).toBool();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	if (ok != Q_NULLPTR) {
		*ok = false;
	}
	return false;
}

/*!
 * @brief Constructs a string containing a comma-separated list
 *     of message identifiers.
 *
 * @param[in] msgIds Message identifiers.
 * @return String with list or empty string when list empty or on failure.
 */
static
QString toListString(const QList<MsgId> &msgIds)
{
	QStringList list;
	foreach (const MsgId &msgId, msgIds) {
		if (Q_UNLIKELY(msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}
		list.append(QString::number(msgId.dmId()));
	}
	if (!list.isEmpty()) {
		return list.join(", ");
	}
	return QString();
}

/*!
 * @brief Get all messages from list not matching the read status.
 *
 * @param[in] query SQL query to work with.
 * @param[in] msgIds Message identifiers to be checked.
 * @param[in] read Locally read status.
 * @return Message identifier list not matching the read status.
 */
static
QList<MsgId> notSatisfyingRead(QSqlQuery &query, const QList<MsgId> &msgIds,
    bool read)
{
	QString queryStr;
	{
		QString idListing = toListString(msgIds);
		if (Q_UNLIKELY(idListing.isEmpty())) {
			return QList<MsgId>();
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString("SELECT m.dmId, m.dmDeliveryTime "
		    "FROM messages AS m "
		    "LEFT JOIN supplementary_message_data AS s "
		    "ON (m.dmID = s.message_id) "
		    "WHERE (s.read_locally != :read) AND (m.dmID IN (%1))")
		        .arg(idListing);
	}

	QList<MsgId> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":read", read);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<MsgId>();
}

bool MessageDb::setMessagesLocallyRead(const QList<MsgId> &msgIds, bool read)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr;
	QList<MsgId> toBeChanged = notSatisfyingRead(query, msgIds, read);
	{
		if (toBeChanged.isEmpty()) {
			/* Same values already stored. */
			return true;
		}

		QString idListing = toListString(toBeChanged);
		if (Q_UNLIKELY(idListing.isEmpty())) {
			Q_ASSERT(0);
			return false;
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString("UPDATE supplementary_message_data "
		    "SET read_locally = :read WHERE message_id IN (%1)")
		        .arg(idListing);
	}

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":read", read);
	if (query.exec()) {
		emit changedLocallyRead(toBeChanged, read);
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Get all messages not matching the read status.
 *
 * @param[in] query SQL query to work with.
 * @param[in] read Locally read status.
 * @return Message identifier list not matching the read status.
 */
static
QList<MsgId> allReceivedNotSatisfyingRead(QSqlQuery &query, bool read)
{
	QString queryStr = "SELECT m.dmId, m.dmDeliveryTime "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE (s.read_locally != :read) AND (s.message_type = :message_type)";

	QList<MsgId> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":read", read);
	query.bindValue(":message_type", MessageDb::TYPE_RECEIVED);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<MsgId>();
}

bool MessageDb::smsgdtSetAllReceivedLocallyRead(bool read)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QList<MsgId> toBeChanged = allReceivedNotSatisfyingRead(query, read);
	if (toBeChanged.isEmpty()) {
		/* Same values already stored. */
		return true;
	}

	QString queryStr = "UPDATE supplementary_message_data "
	    "SET read_locally = :read WHERE message_type = :message_type";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":read", read);
	query.bindValue(":message_type", TYPE_RECEIVED);
	if (query.exec()) {
		emit changedLocallyRead(toBeChanged, read);
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Get all messages not matching the read status.
 *
 * @param[in] query SQL query to work with.
 * @param[in] year Year number.
 * @param[in] read Locally read status.
 * @return Message identifier list not matching the read status.
 */
static
QList<MsgId> allReceivedYearNotSatisfyingRead(QSqlQuery &query,
    const QString &year, bool read)
{
	QString queryStr = "SELECT m.dmId, m.dmDeliveryTime "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE (s.read_locally != :read) AND (s.message_type = :message_type) AND "
	    "(ifnull(strftime('%Y', m.dmDeliveryTime), '" INVALID_YEAR "') = :year)";

	QList<MsgId> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":read", read);
	query.bindValue(":message_type", MessageDb::TYPE_RECEIVED);
	query.bindValue(":year", year);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<MsgId>();
}

bool MessageDb::smsgdtSetReceivedYearLocallyRead(const QString &year,
    bool read)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QList<MsgId> toBeChanged = allReceivedYearNotSatisfyingRead(query,
	    year, read);
	if (toBeChanged.isEmpty()) {
		/* Same values already stored. */
		return true;
	}

	QString queryStr = "INSERT OR REPLACE INTO supplementary_message_data ("
	    "message_id, message_type, read_locally, custom_data)"
	    " SELECT s.message_id, s.message_type, :read, s.custom_data "
	    "FROM supplementary_message_data AS s "
	    "LEFT JOIN messages AS m ON (s.message_id = m.dmID) "
	    "WHERE (ifnull(strftime('%Y', m.dmDeliveryTime), '" INVALID_YEAR "') = :year) and "
	    "(s.message_type = :message_type)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":read", read);
	query.bindValue(":year", nullVariantWhenIsNull(year));
	query.bindValue(":message_type", TYPE_RECEIVED);
	if (query.exec()) {
		emit changedLocallyRead(toBeChanged, read);
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Get all messages delivered within 90 days not matching the read status.
 *
 * @param[in] query SQL query to work with.
 * @param[in] read Locally read status.
 * @return Message identifier list not matching the read status.
 */
static
QList<MsgId> allReceivedWithin90DaysNotSatisfyingRead(QSqlQuery &query,
    bool read)
{
	QString queryStr = "SELECT m.dmId, m.dmDeliveryTime "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE (s.read_locally != :read) AND (s.message_type = :message_type) AND "
	    "(m.dmDeliveryTime >= date('now','-90 day'))";

	QList<MsgId> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":read", read);
	query.bindValue(":message_type", MessageDb::TYPE_RECEIVED);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<MsgId>();
}

bool MessageDb::smsgdtSetWithin90DaysReceivedLocallyRead(bool read)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QList<MsgId> toBeChanged = allReceivedWithin90DaysNotSatisfyingRead(
	    query, read);
	if (toBeChanged.isEmpty()) {
		/* Same values already stored. */
		return true;
	}

	QString queryStr = "INSERT OR REPLACE INTO supplementary_message_data ("
	    "message_id, message_type, read_locally, custom_data)"
	    " SELECT s.message_id, s.message_type, :read, s.custom_data "
	    "FROM supplementary_message_data AS s "
	    "LEFT JOIN messages AS m ON (s.message_id = m.dmID) "
	    "WHERE (m.dmDeliveryTime >= date('now','-90 day')) and "
	    "(s.message_type = :message_type)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":read", read);
	query.bindValue(":message_type", TYPE_RECEIVED);
	if (query.exec()) {
		emit changedLocallyRead(toBeChanged, read);
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

MsgId MessageDb::msgsMsgId(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	MsgId ret;
	QString queryStr = "SELECT dmID, dmDeliveryTime FROM messages "
	    "WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (Q_UNLIKELY(!(query.exec() && query.isActive()))) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.first() && query.isValid()) {
		ret.setDmId(query.value(0).toLongLong());
		ret.setDeliveryTime(
		    dateTimeFromDbFormat(query.value(1).toString()));
	} else {
		logWarningNL("Cannot read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return ret;
}

QList<MessageDb::ContactEntry> MessageDb::uniqueContacts(void)
{
	QMutexLocker locker(&m_lock);

	QMap<QString, ContactEntry> mapOfBoxes;
	QList<ContactEntry> contactList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT m.dmID AS id, m.dbIDRecipient, "
	    "m.dmRecipient, m.dmRecipientAddress "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE "
	    "(s.message_type = :message_type_sent)"
	    " and "
	    "(m.dmRecipientAddress IS NOT NULL)"
	    " UNION "
	    "SELECT m.dmID AS id, m.dbIDSender, m.dmSender, "
	    "m.dmSenderAddress "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE (s.message_type = :message_type_received) and "
	    "(m.dmSenderAddress IS NOT NULL) ORDER BY m.dmID DESC";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type_sent", TYPE_SENT);
	query.bindValue(":message_type_received", TYPE_RECEIVED);
	if (query.exec() && query.isActive()) {
		query.first();
		ContactEntry newEntry;
		while (query.isValid()) {
			newEntry.dmId = query.value(0).toLongLong();
			newEntry.boxId = query.value(1).toString();
			newEntry.name = query.value(2).toString();
			newEntry.address = query.value(3).toString();
			QMap<QString, ContactEntry>::iterator found = mapOfBoxes.find(newEntry.boxId);
			if (mapOfBoxes.end() == found) {
				mapOfBoxes.insert(newEntry.boxId, newEntry);
			} else if (found->dmId < newEntry.dmId) {
				/* Prefer entries with larger ids. */
				*found = newEntry;
			}
			query.next();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	foreach (const ContactEntry &entry, mapOfBoxes) {
		contactList.append(entry);
	}
fail:
	return contactList;
}

int MessageDb::daysRemainingSinceAcceptance(const QDateTime &acceptanceTime,
    const QDate &toDate, int givenLimit)
{
	if (Q_UNLIKELY(!acceptanceTime.isValid())) {
		return -1;
	}

	const QDate acceptanceDate(acceptanceTime.date());
	/*
	 * The message delete/move operation is started past midnight for
	 * those messages which, according to the interpretation of the law,
	 * are to be deleted that day (because more than 90 full days have
	 * passed after their delivery).
	 * A message is usually deleted on the 91st day since acceptance.
	*/
	int remainsDays = givenLimit - acceptanceDate.daysTo(toDate);
	return (remainsDays > 0) ? remainsDays : 0;
}

QDateTime MessageDb::messageAcceptanceTime(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr =
	    "SELECT dmAcceptanceTime FROM messages WHERE dmID = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return dateTimeFromDbFormat(query.value(0).toString());
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QDateTime();
}

QString MessageDb::descriptionHtml(qint64 dmId, const QString &topInsertedLine,
    bool verSignature)
{
	QMutexLocker locker(&m_lock);

	QString html;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;

	html += indentDivStart;

	if (!topInsertedLine.isEmpty()) {
		html += "<br/>" % topInsertedLine;
	}

	html += "<h3>" + tr("Identification") + "</h3>";
	html += strongAccountInfoLine(tr("Message ID"), QString::number(dmId));

	queryStr = "SELECT dmAnnotation, _dmType, dmSender, dmSenderAddress, "
	    "dmRecipient, dmRecipientAddress, dbIDSender, dbIDRecipient, "
	    "dmSenderType FROM messages WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		html += strongAccountInfoLine(tr("Subject"),
		    query.value(0).toString());
		if (!query.value(1).toString().isEmpty() &&
		    (!Isds::Description::descrDmTypeChar(query.value(1).toString()).isEmpty())) {
			const QString typeStr(query.value(1).toString());
			html += strongAccountInfoLine(tr("Message type"),
			    typeStr + QStringLiteral(" - ") + Isds::Description::descrDmTypeChar(typeStr));
		}

		html += "<br/>";

		/* Information about message author. */
		html += strongAccountInfoLine(tr("Sender"),
		    query.value(2).toString());

		html += strongAccountInfoLine(tr("Sender Databox ID"),
		    query.value(6).toString());

		QString dmSenderType =
		    Isds::Description::descrDbType(Isds::intVariant2DbType(query.value(8)));
		if (dmSenderType != "") {
			html += strongAccountInfoLine(tr("Databox type"),
			    dmSenderType);
		}

		html += strongAccountInfoLine(tr("Sender Address"),
		    query.value(3).toString());
		/* Custom data. */
		QJsonDocument customData = getMessageCustomData(dmId);
		if (!customData.isEmpty() && customData.isObject()) {
			QJsonValue value =
			    customData.object().value("message_author");
			if (value.isObject()) {
				QString authorInfo = value.toObject().value(
				        "authorName").toString();
				if (!authorInfo.isEmpty()) {
					authorInfo += ", ";
				}
				html += strongAccountInfoLine(
				    tr("Message author"),
				    authorInfo +
				    Isds::Description::descrSenderType(
				        Isds::variant2SenderType(value.toObject().value("userType"))));
			}
		}

		html += "<br/>";

		html += strongAccountInfoLine(tr("Recipient"),
		    query.value(4).toString());
		html += strongAccountInfoLine(
		    tr("Recipient Databox ID"),
		    query.value(7).toString());
		html += strongAccountInfoLine(tr("Recipient Address"),
		    query.value(5).toString());
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	queryStr = "SELECT ";
	for (int i = 0; i < (msgPrintedAttribs.size() - 1); ++i) {
		queryStr += msgPrintedAttribs[i] + ", ";
	}
	queryStr += msgPrintedAttribs.last();
	queryStr += " FROM messages WHERE "
	    "dmID = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		for (int i = 0; i < msgPrintedAttribs.size(); ++i) {
			if (!query.value(i).toString().isEmpty()) {
				html += strongAccountInfoLine(
				    msgsTbl.attrProps[msgPrintedAttribs[i]].desc,
				    query.value(i).toString());
			}
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	queryStr = "SELECT ";
	for (int i = 0; i < (msgDeliveryBoolAttribs.size() - 1); ++i) {
		queryStr += msgDeliveryBoolAttribs[i] + ", ";
	}
	queryStr += msgDeliveryBoolAttribs.last();
	queryStr += " FROM messages WHERE "
	    "dmID = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		for (int i = 0; i < msgDeliveryBoolAttribs.size(); ++i) {
			html += strongAccountInfoLine(
			    msgsTbl.attrProps[msgDeliveryBoolAttribs[i]].desc,
			    (query.value(i).toBool()) ? tr("Yes") : tr("No"));
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}


	html += "<h3>" + tr("Status") + "</h3>";

	/* Message Status. */
	queryStr = "SELECT ";
	for (int i = 0; i < (msgStatus.size() - 1); ++i) {
		queryStr += msgStatus[i] + ", ";
	}
	queryStr += msgStatus.last();
	queryStr += " FROM messages WHERE "
	    "dmID = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		html += strongAccountInfoLine(
		    msgsTbl.attrProps[msgStatus[0]].desc,
		    dateTimeStrFromDbFormat(query.value(0).toString(),
		        dateTimeDisplayFormat));
		html += strongAccountInfoLine(
		    msgsTbl.attrProps[msgStatus[1]].desc,
		    dateTimeStrFromDbFormat(query.value(1).toString(),
		        dateTimeDisplayFormat));
		html += strongAccountInfoLine(
		    msgsTbl.attrProps[msgStatus[2]].desc,
		    QString::number(query.value(2).toInt()) + " -- " +
		    Isds::Description::descrDmState(Isds::variant2DmState(query.value(2))));
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Events. */
	queryStr = "SELECT "
	    "dmEventTime, dmEventDescr"
	    " FROM events WHERE "
	    "message_id = :dmId"
	    " ORDER BY dmEventTime ASC";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			html += strongAccountInfoLine(tr("Events"), QString());
		}
		while (query.isValid()) {
			html += indentDivStart +
			    strongAccountInfoLine(
			        dateTimeStrFromDbFormat(
			            query.value(0).toString(),
			            dateTimeDisplayFormat),
			        query.value(1).toString()) +
			    divEnd;
			query.next();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Attachments. */
	queryStr = "SELECT COUNT(*) AS nrFiles "
	    " FROM files WHERE "
	    "message_id = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid() &&
	    (query.value(0).toInt() > 0)) {
		html += strongAccountInfoLine(tr("Attachments"),
		    QString::number(query.value(0).toInt()) + " " +
		    tr("(downloaded and ready)"));
	} else {
		queryStr = "SELECT "
		    "dmAttachmentSize"
		    " FROM messages WHERE "
		    "dmID = :dmId";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":dmId", dmId);
		if (query.exec() && query.isActive()) {
			query.first();
			if (query.isValid() && (query.value(0).toInt() > 0)) {
			html += strongAccountInfoLine(
			    tr("Attachments"),
			    tr("not downloaded yet, ~") +
			    QString::number(query.value(0).toInt()) +
			    tr(" KB; use 'Download' to get them."));
			} else {
				html += strongAccountInfoLine(
				    tr("Attachments"), tr("(not available)"));
			}
		} else {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
	}

	if (verSignature) {

		html += "<h3>" + tr("Signature") + "</h3>";

		/* Signature. */
		bool verified = false;
		QString verifiedText;
		MessageDb::MsgVerificationResult vRes = isMessageVerified(dmId);

		switch (vRes) {
		case MessageDb::MSG_SIG_BAD:
			html += strongAccountInfoLine(tr("Message signature"),
			    tr("Invalid")  + " -- " +
			    tr("Message signature and content do not correspond!")
			    );
			break;
		case MessageDb::MSG_SIG_OK:
			html += strongAccountInfoLine(tr("Message signature"),
			    tr("Valid"));
			/* Check signing certificate. */
			verified = msgCertValidAtDate(dmId,
			    msgsVerificationDate(dmId),
			    !PrefsSpecific::checkCrl(*GlobInstcs::prefsPtr));
			verifiedText = verified ? tr("Valid") : tr("Invalid");
			if (!PrefsSpecific::checkCrl(*GlobInstcs::prefsPtr)) {
				verifiedText += " (" +
				    tr("Certificate revocation check is turned off!")
				    + ")";
			}
			html += strongAccountInfoLine(
			    tr("Signing certificate"), verifiedText);
			break;
		default:
			/* Verification no attempted. */
			html += strongAccountInfoLine(tr("Message signature"),
			    tr("Not present"));
			html += "<div>" +
			    tr("Download the complete message in order to verify its signature.") +
			    "</div>";
			break;
		}

		{
			/* Time-stamp. */
			QDateTime tst;
			QByteArray tstData = getMessageTimestampRaw(dmId);
			QString timeStampStr;
			if (tstData.isEmpty()) {
				timeStampStr = tr("Not present");
			} else {
				int64_t utc_time = 0;
				int ret = raw_tst_verify(tstData.data(),
				    tstData.size(), &utc_time);

				if (-1 != ret) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
					tst = QDateTime::fromSecsSinceEpoch(utc_time);
#else /* < Qt-5.8 */
					tst = QDateTime::fromMSecsSinceEpoch(utc_time * 1000);
#endif /* >= Qt-5.8 */
				}

				timeStampStr =
				    (1 == ret) ? tr("Valid") : tr("Invalid");
				if (-1 != ret) {
					timeStampStr +=
					    " (" +
					    tst.toString("dd.MM.yyyy hh:mm:ss")
					    + " " +
					    tst.timeZone().abbreviation(tst) +
					    ")";
				}
			}
			html += strongAccountInfoLine(tr("Time stamp"),
			    timeStampStr);
			if (tstData.isEmpty()) {
				html += "<div>" +
				    tr("Download the complete message in order to verify its time stamp.") +
				    "</div>";
			}
		}
	}

	html += divEnd;
	return html;
fail:
	return QString();
}

/*!
 * @brief Determine message type.
 *
 * @param[in]  db Database reference.
 * @param[in]  msgId Message identifier.
 * @param[out] ok Set to true if valid value found.
 * @return Whether message is rent or received.
 */
static
enum MessageDb::MessageType messageType(const QSqlDatabase &db, qint64 dmId,
    bool *ok = Q_NULLPTR)
{
	QSqlQuery query(db);

	QString queryStr("SELECT message_type "
	    "FROM supplementary_message_data WHERE message_id = :dmId");
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		int val = query.value(0).toInt();
		if (val == MessageDb::TYPE_RECEIVED) {
			if (ok != Q_NULLPTR) {
				*ok = true;
			}
			return MessageDb::TYPE_RECEIVED;
		} else if (val == MessageDb::TYPE_SENT) {
			if (ok != Q_NULLPTR) {
				*ok = true;
			}
			return MessageDb::TYPE_SENT;
		} else {
			goto fail;
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	/* Assume received. */
	logErrorNL("Cannot determine type of message '%" PRId64 "'. Assuming received.",
	    UGLY_QINT64_CAST dmId);
	if (ok != Q_NULLPTR) {
		*ok = false;
	}
	return MessageDb::TYPE_RECEIVED;
}

QString MessageDb::envelopeInfoHtmlToPdf(qint64 dmId, const QString &dbType)
{
	QMutexLocker locker(&m_lock);

	QString html;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;
	QString tmp, readStr, label;

	bool typeOk = false;
	enum MessageType msgType =
	    messageType(DelayedAccessSQLiteDb::accessDb(), dmId, &typeOk);

	html += indentDivStart;

	html += "<table width=\"100%\" style=\"padding: 30px 30px 30px 30px; "
	    "font-size: 20px;\"><tr><td>" +
	    strongMessagePdf(tr("Envelope")) +
	    "</td><td align=\"right\">" +
	    tr("Message ID:") + " " +
	    strongMessagePdf(QString::number(dmId)) +
	    "</td></tr></table><br/><br/>";

	queryStr = "SELECT "
	    "dmSender, dmSenderAddress, dbIDSender, _dmType, "
	    "dmRecipient, dmRecipientAddress, dmDeliveryTime, dmAnnotation, "
	    "dmLegalTitleLaw, dmLegalTitleYear, dmLegalTitleSect, "
	    "dmLegalTitlePar, dmLegalTitlePoint, "
	    "dmSenderRefNumber, dmSenderIdent, "
	    "dmRecipientRefNumber, dmRecipientIdent, "
	    "dmToHands, dmPersonalDelivery, dmAllowSubstDelivery, "
	    "dbIDRecipient "
	    "FROM messages WHERE "
	    "dmID = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {

		/* Sender info */
		html += messageTableSectionPdf(tr("Sender"));
		html += messageTableInfoStartPdf();
		tmp = query.value(0).toString() % QStringLiteral(", ") %
		    query.value(1).toString();
		html += messageTableInfoPdf(tr("Name"), tmp);
		html += messageTableInfoPdf(tr("Databox ID"),
		    query.value(2).toString());
		html += messageTableInfoPdf(tr("Databox Type"),
		    dbType);
		html += messageTableInfoEndPdf();

		/* Recipient info */
		html += messageTableSectionPdf(tr("Recipient"));
		html += messageTableInfoStartPdf();
		tmp = query.value(4).toString() % QStringLiteral(", ") %
		    query.value(5).toString();
		html += messageTableInfoPdf(tr("Name"), tmp);
		html += messageTableInfoPdf(tr("Databox ID"),
		    query.value(20).toString());
		html += messageTableInfoPdf(tr("Delivery"),
		        dateTimeStrFromDbFormat(query.value(6).toString(),
		        dateTimeDisplayFormat));
		html += messageTableInfoEndPdf();

		/* General info */
		html += messageTableSectionPdf(tr("General Information"));
		html += messageTableInfoStartPdf();
		html += messageTableInfoPdf(tr("Subject"),
		    query.value(7).toString());

		tmp = query.value(8).toString();
		if (tmp.isEmpty()) {
			tmp = QStringLiteral("0");
		}
		tmp += QStringLiteral(" / ");
		readStr = query.value(9).toString();
		tmp += readStr.isEmpty() ? QStringLiteral("0") : readStr;
		tmp += QStringLiteral(" §  ") % query.value(10).toString() %
		    QStringLiteral(" ") % tr("paragraph") %
		    QStringLiteral(" ") % query.value(11).toString() %
		    QStringLiteral(" ") % tr("letter") %
		    QStringLiteral(" ") % query.value(12).toString();

		html += messageTableInfoPdf(tr("Delegation"), tmp);

		/*
		 * 13 dmSenderRefNumber
		 * 14 dmSenderIdent
		 * 15 dmRecipientRefNumber
		 * 16 dmRecipientIdent
		 */

		readStr = query.value(13).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		label = tr("Sender ref. num.");
		if (typeOk) {
			label = tr("Sender (%1) ref. num.")
			    .arg((msgType == TYPE_RECEIVED) ? tr("your") : tr("our"));
		}
		html += messageTableInfoPdf(label, tmp);

		readStr = query.value(14).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		label = tr("Sender file mark");
		if (typeOk) {
			label = tr("Sender (%1) file mark")
			    .arg((msgType == TYPE_RECEIVED) ? tr("your") : tr("our"));
		}
		html += messageTableInfoPdf(label, tmp);

		readStr = query.value(15).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		label = tr("Recipient ref. num.");
		if (typeOk) {
			label = tr("Recipient (%1) ref. num.")
			    .arg((msgType == TYPE_RECEIVED) ? tr("our") : tr("your"));
		}
		html += messageTableInfoPdf(label, tmp);

		readStr = query.value(16).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		label = tr("Recipient file mark");
		if (typeOk) {
			label = tr("Recipient (%1) file mark")
			    .arg((msgType == TYPE_RECEIVED) ? tr("our") : tr("your"));
		}
		html += messageTableInfoPdf(label, tmp);

		readStr = query.value(17).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		html += messageTableInfoPdf(tr("To hands"), tmp);

		if (query.value(18).toInt()) {
			tmp = tr("yes");
		} else {
			tmp = tr("no");
		}
		html += messageTableInfoPdf(tr("Personal Delivery"), tmp);

		if (query.value(19).toInt()) {
			tmp = tr("no");
		} else {
			tmp = tr("yes");
		}
		html += messageTableInfoPdf(
		    tr("Prohibit Acceptance through Fiction"), tmp);

		html += messageTableInfoEndPdf();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	queryStr = "SELECT _dmFileDescr FROM files WHERE message_id = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		/* Attachments info */
		html += messageTableSectionPdf(tr("Attachments"));
		html += messageTableInfoStartPdf();
		int i = 1;
		while (query.isValid()) {
			html += messageTableInfoPdf(QString::number(i),
			query.value(0).toString());
			query.next();
			i++;
		}
		html += messageTableInfoEndPdf();
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	html += divEnd;

	return html;

fail:
	return QString();
}

QString MessageDb::fileListHtmlToPdf(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QString html;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT _dmFileDescr FROM files "
	    "WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		/* Attachments info */
		html += "<br/>";
		html += "<h3>" + tr("List of attachments") + "</h3>";
		int i = 1;
		while (query.isValid()) {
			html += strongAccountInfoLine(QString::number(i),
			query.value(0).toString());
			query.next();
			i++;
		}
		return html;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QString();
}

QString MessageDb::deliveryInfoHtmlToPdf(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QString html;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;
	QString tmp, readStr, label;

	bool typeOk = false;
	enum MessageType msgType =
	    messageType(DelayedAccessSQLiteDb::accessDb(), dmId, &typeOk);

	html += indentDivStart;

	html += "<table width=\"100%\" style=\"padding: 30px 30px 30px 30px; "
	    "font-size: 20px;\"><tr><td>" +
	    strongMessagePdf(tr("Advice of Acceptance")) +
	    "</td><td align=\"right\">" +
	    tr("Message ID:") + " " +
	    strongMessagePdf(QString::number(dmId)) +
	    "</td></tr></table><br/><br/>";

	queryStr = "SELECT "
	    "dmSender, dmSenderAddress, dbIDSender, _dmType, "
	    "dmRecipient, dmRecipientAddress, dmDeliveryTime, dmAnnotation, "
	    "dmLegalTitleLaw, dmLegalTitleYear, dmLegalTitleSect, "
	    "dmLegalTitlePar, dmLegalTitlePoint, "
	    "dmSenderRefNumber, dmSenderIdent, "
	    "dmRecipientRefNumber, dmRecipientIdent, "
	    "dmToHands, dmPersonalDelivery, dmAllowSubstDelivery, "
	    "dmAcceptanceTime, dbIDSender, dbIDRecipient "
	    "FROM messages WHERE "
	    "dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {

		/* Sender info */
		html += messageTableSectionPdf(tr("Sender"));
		html += messageTableInfoStartPdf();
		tmp = query.value(0).toString() % QStringLiteral(" (") %
		    query.value(21).toString() % QStringLiteral("), ") %
		    query.value(1).toString();
		html += messageTableInfoPdf(tr("Name"), tmp);
		html += messageTableInfoEndPdf();

		/* Recipient info */
		html += messageTableSectionPdf(tr("Recipient"));
		html += messageTableInfoStartPdf();
		tmp = query.value(4).toString() % QStringLiteral(" (") %
		    query.value(22).toString() % QStringLiteral("), ") %
		    query.value(5).toString();
		html += messageTableInfoPdf(tr("Name"), tmp);
		html += messageTableInfoEndPdf();

		/* General info */
		html += messageTableSectionPdf(tr("General Information"));
		html += messageTableInfoStartPdf();
		html += messageTableInfoPdf(tr("Subject"),
		    query.value(7).toString());

		tmp = query.value(8).toString();
		if (tmp.isEmpty()) {
			tmp = QStringLiteral("0");
		}
		tmp += QStringLiteral(" / ");
		readStr = query.value(9).toString();
		tmp += readStr.isEmpty() ? QStringLiteral("0") : readStr;
		tmp += QStringLiteral(" §  ") % query.value(10).toString() %
		    QStringLiteral(" ") % tr("paragraph") %
		    QStringLiteral(" ") % query.value(11).toString() %
		    QStringLiteral(" ") % tr("letter") %
		    QStringLiteral(" ") % query.value(12).toString();

		html += messageTableInfoPdf(tr("Delegation"), tmp);

		/*
		 * 13 dmSenderRefNumber
		 * 14 dmSenderIdent
		 * 15 dmRecipientRefNumber
		 * 16 dmRecipientIdent
		 */

		readStr = query.value(13).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		label = tr("Sender ref. num.");
		if (typeOk) {
			label = tr("Sender (%1) ref. num.")
			    .arg((msgType == TYPE_RECEIVED) ? tr("your") : tr("our"));
		}
		html += messageTableInfoPdf(label, tmp);

		readStr = query.value(14).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		label = tr("Sender file mark");
		if (typeOk) {
			label = tr("Sender (%1) file mark")
			    .arg((msgType == TYPE_RECEIVED) ? tr("your") : tr("our"));
		}
		html += messageTableInfoPdf(label, tmp);

		readStr = query.value(15).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		label = tr("Recipient ref. num.");
		if (typeOk) {
			label = tr("Recipient (%1) ref. num.")
			    .arg((msgType == TYPE_RECEIVED) ? tr("our") : tr("your"));
		}
		html += messageTableInfoPdf(label, tmp);

		readStr = query.value(16).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		label = tr("Recipient file mark");
		if (typeOk) {
			label = tr("Recipient (%1) file mark")
			    .arg((msgType == TYPE_RECEIVED) ? tr("our") : tr("your"));
		}
		html += messageTableInfoPdf(label, tmp);

		readStr = query.value(17).toString();
		tmp = readStr.isEmpty() ? tr("Not specified") : readStr;
		html += messageTableInfoPdf(tr("To hands"), tmp);

		if ((query.value(18)).toInt()) {
			tmp = tr("yes");
		} else {
			tmp = tr("no");
		}
		html += messageTableInfoPdf(tr("Personal Delivery"), tmp);

		if (query.value(19).toInt()) {
			tmp = tr("no");
		} else {
			tmp = tr("yes");
		}
		html += messageTableInfoPdf(
		    tr("Prohibit Acceptance through Fiction"), tmp);

		html += messageTableInfoEndPdf();

		/* Delivery info */
		html += messageTableSectionPdf(
		    tr("Delivery/Acceptance Information"));
		html += messageTableInfoStartPdf();
		html += messageTableInfoPdf(tr("Delivery"),
		        dateTimeStrFromDbFormat(query.value(6).toString(),
		        dateTimeDisplayFormat));
		html += messageTableInfoPdf(tr("Acceptance"),
		        dateTimeStrFromDbFormat(query.value(20).toString(),
		        dateTimeDisplayFormat));
		html += messageTableInfoEndPdf();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	queryStr = "SELECT dmEventTime, dmEventDescr "
	    "FROM events WHERE message_id = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		/* Attachments info */
		html += messageTableSectionPdf(tr("Events"));
		html += messageTableInfoStartPdf();
		if (Q_UNLIKELY(!query.isValid())) {
			goto fail;
		}
		while (query.isValid()) {
			tmp = dateTimeStrFromDbFormat(
			    query.value(0).toString(),
			    dateTimeDisplayFormat) + " - " +
			    query.value(1).toString();
			html += messageTableInfoPdf(tr("Time"), tmp);
			query.next();
		}
		html += messageTableInfoEndPdf();
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	html += divEnd;

	return html;

fail:
	return QString();
}

/*
 * The following values have historically been stored into the database.
 * We still must be able to read them.
 */
static const QString strEncl("encl"), strSign("sign");

/*!
 * @brief Converts string as used in database to type.
 *
 * @param[in] s String as used in the database.
 * @return File meta type.
 */
static
enum Isds::Type::FileMetaType dbStr2FileMetaType(const QString &s)
{
	if (s.isEmpty()) {
		return Isds::Type::FMT_UNKNOWN;
	} else if (s == strEncl) {
		return Isds::Type::FMT_ENCLOSURE;
	} else if (s == strSign) {
		return Isds::Type::FMT_SIGNATURE;
	} else {
		return Isds::str2FileMetaType(s);
	}
}

/*!
 * @brief Converts type to string as used in database.
 *
 * @param[in] fmt File meta type.
 * @return String as used in the database.
 */
static
const QString &fileMetaType2DbStr(enum Isds::Type::FileMetaType fmt)
{
	switch (fmt) {
	case Isds::Type::FMT_ENCLOSURE: return strEncl; break;
	case Isds::Type::FMT_SIGNATURE: return strSign; break;
	default:
		return Isds::fileMetaType2Str(fmt);
	}
}

QList<Isds::Document> MessageDb::getMessageAttachments(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QList<Isds::Document> documents;
	QString queryStr = "SELECT _dmFileDescr, _dmUpFileGuid, _dmFileGuid, "
	    "_dmMimeType, _dmFormat, _dmFileMetaType, dmEncodedContent "
	    "FROM files WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			Isds::Document document;
			document.setFileDescr(query.value(0).toString());
			document.setUpFileGuid(query.value(1).toString());
			document.setFileGuid(query.value(2).toString());
			document.setMimeType(query.value(3).toString());
			document.setFormat(query.value(4).toString());
			document.setFileMetaType(dbStr2FileMetaType(query.value(5).toString()));
			document.setBase64Content(query.value(6).toString());
			documents.append(document);
			query.next();
		}
		return documents;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QList<Isds::Document>();
}

QList<MessageDb::AttachmentEntry> MessageDb::attachEntries(qint64 dmId,
    bool withContent)
{
	QMutexLocker locker(&m_lock);

	QList<AttachmentEntry> entryList;
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT ";
	queryStr += joinStrVect(
	    withContent ? fileItemIdsNoSize : fileItemIdsNoDataSize, ", ");
	queryStr += " FROM files WHERE "
	    "message_id = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* First three columns ought to be hidden. */

	query.first();
	while (query.isActive() && query.isValid()) {
		entryList.append(AttachmentEntry(query.value(0).toLongLong(),
		    dmId,
		    withContent ? QByteArray::fromBase64(query.value(1).toByteArray()) : QByteArray(),
		    query.value(2).toString(), query.value(3).toString()));
		query.next();
	}

	return entryList;
fail:
	return QList<AttachmentEntry>();
}

QByteArray MessageDb::getAttachmentContent(qint64 fileId)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr = "SELECT dmEncodedContent FROM files WHERE id = :id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return QByteArray();
	}
	query.bindValue(":id", fileId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			return QByteArray::fromBase64(query.value(0).toByteArray());
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}

	return QByteArray();
}

bool MessageDb::insertMessageEnvelope(const Isds::Envelope &envelope,
    const QString &_origin, enum MessageDirection msgDirect, bool transaction)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;

	if (Q_UNLIKELY(envelope.dmId() < 0)) {
		Q_ASSERT(0);
		logErrorNL("%s",
		    "Cannot insert envelope data with invalid identifier.");
		return false;
	}

	if (transaction) {
		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			logErrorNL("%s", "Cannot begin transaction.");
			goto fail;
		}
	}

	queryStr = "INSERT INTO messages ("
	    "dmID, _origin, dbIDSender, dmSender, "
	    "dmSenderAddress, dmSenderType, dmRecipient, "
	    "dmRecipientAddress, dmAmbiguousRecipient, dmSenderOrgUnit, "
	    "dmSenderOrgUnitNum, dbIDRecipient, dmRecipientOrgUnit, "
	    "dmRecipientOrgUnitNum, dmToHands, dmAnnotation, "
	    "dmRecipientRefNumber, dmSenderRefNumber, dmRecipientIdent, "
	    "dmSenderIdent, dmLegalTitleLaw, dmLegalTitleYear, "
	    "dmLegalTitleSect, dmLegalTitlePar, dmLegalTitlePoint, "
	    "dmPersonalDelivery, dmAllowSubstDelivery, dmQTimestamp, "
	    "dmDeliveryTime, dmAcceptanceTime, dmMessageStatus, "
	    "dmAttachmentSize, _dmType"
	    ") VALUES ("
	    ":dmId, :_origin, :dbIDSender, :dmSender, "
	    ":dmSenderAddress, :dmSenderType, :dmRecipient, "
	    ":dmRecipientAddress, :dmAmbiguousRecipient, :dmSenderOrgUnit, "
	    ":dmSenderOrgUnitNum, :dbIDRecipient, :dmRecipientOrgUnit, "
	    ":dmRecipientOrgUnitNum, :dmToHands, :dmAnnotation, "
	    ":dmRecipientRefNumber, :dmSenderRefNumber, :dmRecipientIdent, "
	    ":dmSenderIdent, :dmLegalTitleLaw, :dmLegalTitleYear, "
	    ":dmLegalTitleSect, :dmLegalTitlePar, :dmLegalTitlePoint,"
	    ":dmPersonalDelivery, :dmAllowSubstDelivery, :dmQTimestamp, "
	    ":dmDeliveryTime, :dmAcceptanceTime, :dmMessageStatus, "
	    ":dmAttachmentSize, :_dmType"
	    ")";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", envelope.dmId());
	query.bindValue(":_origin", nullVariantWhenIsNull(_origin));
	query.bindValue(":dbIDSender", nullVariantWhenIsNull(envelope.dbIDSender()));
	query.bindValue(":dmSender", nullVariantWhenIsNull(envelope.dmSender()));
	query.bindValue(":dmSenderAddress", nullVariantWhenIsNull(envelope.dmSenderAddress()));
	query.bindValue(":dmSenderType", Isds::dbType2IntVariant(envelope.dmSenderType()));
	query.bindValue(":dmRecipient", nullVariantWhenIsNull(envelope.dmRecipient()));
	query.bindValue(":dmRecipientAddress", nullVariantWhenIsNull(envelope.dmRecipientAddress()));
	query.bindValue(":dmAmbiguousRecipient", Isds::nilBool2Variant(envelope.dmAmbiguousRecipient()));
	query.bindValue(":dmSenderOrgUnit", nullVariantWhenIsNull(envelope.dmSenderOrgUnit()));
	query.bindValue(":dmSenderOrgUnitNum", nullVariantWhenIsNull(envelope.dmSenderOrgUnitNumStr()));
	query.bindValue(":dbIDRecipient", nullVariantWhenIsNull(envelope.dbIDRecipient()));
	query.bindValue(":dmRecipientOrgUnit", nullVariantWhenIsNull(envelope.dmRecipientOrgUnit()));
	query.bindValue(":dmRecipientOrgUnitNum", nullVariantWhenIsNull(envelope.dmRecipientOrgUnitNumStr()));
	query.bindValue(":dmToHands", nullVariantWhenIsNull(envelope.dmToHands()));
	query.bindValue(":dmAnnotation", nullVariantWhenIsNull(envelope.dmAnnotation()));
	query.bindValue(":dmRecipientRefNumber", nullVariantWhenIsNull(envelope.dmRecipientRefNumber()));
	query.bindValue(":dmSenderRefNumber", nullVariantWhenIsNull(envelope.dmSenderRefNumber()));
	query.bindValue(":dmRecipientIdent", nullVariantWhenIsNull(envelope.dmRecipientIdent()));
	query.bindValue(":dmSenderIdent", nullVariantWhenIsNull(envelope.dmSenderIdent()));
	query.bindValue(":dmLegalTitleLaw", nullVariantWhenIsNull(envelope.dmLegalTitleLawStr()));
	query.bindValue(":dmLegalTitleYear", nullVariantWhenIsNull(envelope.dmLegalTitleYearStr()));
	query.bindValue(":dmLegalTitleSect", nullVariantWhenIsNull(envelope.dmLegalTitleSect()));
	query.bindValue(":dmLegalTitlePar", nullVariantWhenIsNull(envelope.dmLegalTitlePar()));
	query.bindValue(":dmLegalTitlePoint", nullVariantWhenIsNull(envelope.dmLegalTitlePoint()));
	query.bindValue(":dmPersonalDelivery", Isds::nilBool2Variant(envelope.dmPersonalDelivery()));
	query.bindValue(":dmAllowSubstDelivery", Isds::nilBool2Variant(envelope.dmAllowSubstDelivery()));
	query.bindValue(":dmQTimestamp", (!envelope.dmQTimestamp().isNull()) ? envelope.dmQTimestamp().toBase64() : QVariant());
	query.bindValue(":dmDeliveryTime", nullVariantWhenIsNull(qDateTimeToDbFormat(envelope.dmDeliveryTime())));
	query.bindValue(":dmAcceptanceTime", nullVariantWhenIsNull(qDateTimeToDbFormat(envelope.dmAcceptanceTime())));
	query.bindValue(":dmMessageStatus", Isds::dmState2Variant(envelope.dmMessageStatus()));
	query.bindValue(":dmAttachmentSize", Isds::nonNegativeLong2Variant(envelope.dmAttachmentSize()));
	query.bindValue(":_dmType", (!envelope.dmType().isNull()) ? envelope.dmType() : QVariant());

	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	queryStr = "INSERT INTO supplementary_message_data ("
	    "message_id, message_type, read_locally, download_date, "
	    "custom_data) VALUES (:dmId, :message_type, :read_locally, "
	    ":download_date, :custom_data)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", envelope.dmId());
	if (MSG_RECEIVED == msgDirect) {
		query.bindValue(":message_type", TYPE_RECEIVED);
		query.bindValue(":read_locally", false);
	} else {
		query.bindValue(":message_type", TYPE_SENT);
		query.bindValue(":read_locally", true);
	}
	query.bindValue(":download_date",
	    nullVariantWhenIsNull(qDateTimeToDbFormat(QDateTime::currentDateTime())));
	query.bindValue(":custom_data", "null");

	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Set the state directly, without fancy comparison and signalling. */
	queryStr = "INSERT INTO process_state (message_id, state) "
	    "VALUES (:dmId, :state)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", envelope.dmId());
	query.bindValue(":state", (int) UNSETTLED);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (transaction) {
		commitTransaction();
	}

	return true;

fail:
	if (transaction) {
		rollbackTransaction();
	}
	return false;
}

/*!
 * @brief Update message envelope.
 *
 * @param[in] query SQL query to work with.
 * @param[in] envelope Message envelope structure.
 * @param[in] _origin Internal download identifier.
 * @param[in] msgDirect Message orientation.
 * @return True on success.
 */
static
bool updateEnvelope(QSqlQuery &query, const Isds::Envelope &envelope,
    const QString &_origin, enum MessageDirection msgDirect)
{
	QString queryStr = "UPDATE messages SET "
	    "_origin = :_origin, "
	    "dbIDSender = :dbIDSender, dmSender = :dmSender, "
	    "dmSenderAddress = :dmSenderAddress, "
	    "dmSenderType = :dmSenderType, "
	    "dmRecipient = :dmRecipient, "
	    "dmRecipientAddress = :dmRecipientAddress, "
	    "dmAmbiguousRecipient = :dmAmbiguousRecipient, "
	    "dmSenderOrgUnit = :dmSenderOrgUnit, "
	    "dmSenderOrgUnitNum = :dmSenderOrgUnitNum, "
	    "dbIDRecipient = :dbIDRecipient, "
	    "dmRecipientOrgUnit = :dmRecipientOrgUnit, "
	    "dmRecipientOrgUnitNum = :dmRecipientOrgUnitNum, "
	    "dmToHands = :dmToHands, dmAnnotation = :dmAnnotation, "
	    "dmRecipientRefNumber = :dmRecipientRefNumber, "
	    "dmSenderRefNumber = :dmSenderRefNumber, "
	    "dmRecipientIdent = :dmRecipientIdent, "
	    "dmSenderIdent = :dmSenderIdent, "
	    "dmLegalTitleLaw = :dmLegalTitleLaw, "
	    "dmLegalTitleYear = :dmLegalTitleYear, "
	    "dmLegalTitleSect = :dmLegalTitleSect, "
	    "dmLegalTitlePar = :dmLegalTitlePar, "
	    "dmLegalTitlePoint = :dmLegalTitlePoint, "
	    "dmPersonalDelivery = :dmPersonalDelivery, "
	    "dmAllowSubstDelivery = :dmAllowSubstDelivery, "
	    "dmQTimestamp = :dmQTimestamp, "
	    "dmDeliveryTime = :dmDeliveryTime, "
	    "dmAcceptanceTime = :dmAcceptanceTime, "
	    "dmMessageStatus = :dmMessageStatus, "
	    "dmAttachmentSize = :dmAttachmentSize, "
	    "_dmType = :_dmType WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", envelope.dmId());
	query.bindValue(":_origin", nullVariantWhenIsNull(_origin));
	query.bindValue(":dbIDSender", nullVariantWhenIsNull(envelope.dbIDSender()));
	query.bindValue(":dmSender", nullVariantWhenIsNull(envelope.dmSender()));
	query.bindValue(":dmSenderAddress", nullVariantWhenIsNull(envelope.dmSenderAddress()));
	query.bindValue(":dmSenderType", Isds::dbType2IntVariant(envelope.dmSenderType()));
	query.bindValue(":dmRecipient", nullVariantWhenIsNull(envelope.dmRecipient()));
	query.bindValue(":dmRecipientAddress", nullVariantWhenIsNull(envelope.dmRecipientAddress()));
	query.bindValue(":dmAmbiguousRecipient", Isds::nilBool2Variant(envelope.dmAmbiguousRecipient()));
	query.bindValue(":dmSenderOrgUnit", nullVariantWhenIsNull(envelope.dmSenderOrgUnit()));
	query.bindValue(":dmSenderOrgUnitNum", nullVariantWhenIsNull(envelope.dmSenderOrgUnitNumStr()));
	query.bindValue(":dbIDRecipient", nullVariantWhenIsNull(envelope.dbIDRecipient()));
	query.bindValue(":dmRecipientOrgUnit", nullVariantWhenIsNull(envelope.dmRecipientOrgUnit()));
	query.bindValue(":dmRecipientOrgUnitNum", nullVariantWhenIsNull(envelope.dmRecipientOrgUnitNumStr()));
	query.bindValue(":dmToHands", nullVariantWhenIsNull(envelope.dmToHands()));
	query.bindValue(":dmAnnotation", nullVariantWhenIsNull(envelope.dmAnnotation()));
	query.bindValue(":dmRecipientRefNumber", nullVariantWhenIsNull(envelope.dmRecipientRefNumber()));
	query.bindValue(":dmSenderRefNumber", nullVariantWhenIsNull(envelope.dmSenderRefNumber()));
	query.bindValue(":dmRecipientIdent", nullVariantWhenIsNull(envelope.dmRecipientIdent()));
	query.bindValue(":dmSenderIdent", nullVariantWhenIsNull(envelope.dmSenderIdent()));
	query.bindValue(":dmLegalTitleLaw", nullVariantWhenIsNull(envelope.dmLegalTitleLawStr()));
	query.bindValue(":dmLegalTitleYear", nullVariantWhenIsNull(envelope.dmLegalTitleYearStr()));
	query.bindValue(":dmLegalTitleSect", nullVariantWhenIsNull(envelope.dmLegalTitleSect()));
	query.bindValue(":dmLegalTitlePar", nullVariantWhenIsNull(envelope.dmLegalTitlePar()));
	query.bindValue(":dmLegalTitlePoint", nullVariantWhenIsNull(envelope.dmLegalTitlePoint()));
	query.bindValue(":dmPersonalDelivery", Isds::nilBool2Variant(envelope.dmPersonalDelivery()));
	query.bindValue(":dmAllowSubstDelivery", Isds::nilBool2Variant(envelope.dmAllowSubstDelivery()));
	query.bindValue(":dmQTimestamp", (!envelope.dmQTimestamp().isNull()) ? envelope.dmQTimestamp().toBase64() : QVariant());
	query.bindValue(":dmDeliveryTime", nullVariantWhenIsNull(qDateTimeToDbFormat(envelope.dmDeliveryTime())));
	query.bindValue(":dmAcceptanceTime", nullVariantWhenIsNull(qDateTimeToDbFormat(envelope.dmAcceptanceTime())));
	query.bindValue(":dmMessageStatus", Isds::dmState2Variant(envelope.dmMessageStatus()));
	query.bindValue(":dmAttachmentSize", Isds::nonNegativeLong2Variant(envelope.dmAttachmentSize()));
	query.bindValue(":_dmType", (!envelope.dmType().isNull()) ? envelope.dmType() : QVariant());

	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	queryStr = "UPDATE supplementary_message_data SET "
	    "message_type = :message_type "
	    "WHERE message_id = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", envelope.dmId());
	if (MSG_RECEIVED == msgDirect) {
		query.bindValue(":message_type", MessageDb::TYPE_RECEIVED);
	} else {
		query.bindValue(":message_type", MessageDb::TYPE_SENT);
	}
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

bool MessageDb::updateMessageEnvelope(const Isds::Envelope &envelope,
    const QString &_origin, enum MessageDirection msgDirect,
    bool transaction)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	if (transaction) {
		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			logErrorNL("%s", "Cannot begin transaction.");
			return false;
		}
	}

	bool ret = updateEnvelope(query, envelope, _origin, msgDirect);
	if (Q_UNLIKELY(!ret)) {
		goto fail;
	}

	if (transaction) {
		commitTransaction();
	}
	return true;

fail:
	if (transaction) {
		rollbackTransaction();
	}
	return false;
}

QChar MessageDb::getDmType(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr ="SELECT _dmType FROM messages WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return QChar();
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			return Isds::variant2Char(query.value(0));
		} else {
			logWarningNL(
			    "Message type '%" PRId64 "' is not stored in database.",
			    UGLY_QINT64_CAST dmId);
			return QChar();
		}
	} else {
		logErrorNL("Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		return QChar();
	}
}

enum Isds::Type::DmState MessageDb::getMessageStatus(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr =
	    "SELECT dmMessageStatus FROM messages WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return Isds::Type::MS_NULL;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			return Isds::variant2DmState(query.value(0));
		} else {
			logWarningNL(
			    "Status of message '%" PRId64 "' is not stored in database.",
			    UGLY_QINT64_CAST dmId);
			return Isds::Type::MS_NULL;
		}
	} else {
		logErrorNL("Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		return Isds::Type::MS_NULL;
	}
}

/*!
 * @brief Converts integer value to enum MessageDb::MessageType.
 *
 * @param[in]  val Value to be converted.
 * @param[out] ok Set to true on success.
 * @return Message type value.
 */
static
enum MessageDb::MessageType int2messageType(int val, bool *ok = Q_NULLPTR)
{
	enum MessageDb::MessageType type = MessageDb::TYPE_RECEIVED;

	switch (val) {
	case MessageDb::TYPE_RECEIVED:
		type = MessageDb::TYPE_RECEIVED;
		break;
	case MessageDb::TYPE_SENT:
		type = MessageDb::TYPE_SENT;
		break;
	default:
		Q_ASSERT(0);
		if (ok != Q_NULLPTR) {
			*ok = false;
		}
		return type;
	}

	if (ok != Q_NULLPTR) {
		*ok = true;
	}
	return type;
}

QList<MessageDb::SoughtMsg> MessageDb::msgsSearch(const Isds::Envelope &envel,
    enum MessageDirection msgDirect, const QString &attachPhrase,
    bool logicalAnd)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	int i = 0;
	bool isMultiSelect = false;
	const QString logicalToken(logicalAnd ? " AND " : " OR ");
	const QString separ(" ");

	const QStringList dmAnnotationList(
	    envel.dmAnnotation().split(separ, _Qt_SkipEmptyParts));
	const QStringList dmSenderList(
	    envel.dmSender().split(separ, _Qt_SkipEmptyParts));
	const QStringList dmAddressList(
	    envel.dmSenderAddress().split(separ, _Qt_SkipEmptyParts));
	const QStringList dmRecipientList(
	    envel.dmRecipient().split(separ, _Qt_SkipEmptyParts));
	const QStringList dmToHandsList(
	    envel.dmToHands().split(separ, _Qt_SkipEmptyParts));
	const QStringList dmFileNameList(
	    attachPhrase.split(separ, _Qt_SkipEmptyParts));
	QList<SoughtMsg> msgList;

	/* Always ask for message type. */
	QString queryStr("SELECT DISTINCT "
	    "m.dmID, m.dmDeliveryTime, "
	    "m.dmAnnotation, m.dmSender, m.dmRecipient, "
	    "s.message_type, m.dmAcceptanceTime, "
	    "(ifnull(r.message_id, 0) != 0) AS is_downloaded "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "LEFT JOIN raw_message_data AS r "
	    "ON (m.dmId = r.message_id) "
	    "LEFT JOIN files AS f "
	    "ON (m.dmID = f.message_id) "
	    "WHERE ");

	if (MSG_ALL == msgDirect) {
		/* select from all messages */
	} else if ((MSG_RECEIVED == msgDirect) || (MSG_SENT == msgDirect)) {
		/* means select only received (1) or sent (2) messages */
		isMultiSelect = true;
	} else {
		/* Wrong input values from search dialogue. */
		return msgList;
	}

	if (envel.dmId() < 0) {

		bool isNotFirst = false;

		if (isMultiSelect) {
			queryStr += "s.message_type = :message_type";
			isNotFirst = true;
		}

		if (!envel.dbIDSender().isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dbIDSender = :dbIDSender";
		} else if (!dmSenderList.isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dmSender LIKE "
			    "'%'||:dmSender0||'%'";
			for (i = 1; i < dmSenderList.count(); i++) {
				queryStr += " AND m.dmSender LIKE "
				    "'%'||:dmSender" + QString::number(i) +
				    "||'%'";
			}
		}

		if (!envel.dbIDRecipient().isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dbIDRecipient = :dbIDRecipient";
		} else if (!dmRecipientList.isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dmRecipient LIKE "
			    "'%'||:dmRecipient0||'%'";
			for (i = 1; i < dmRecipientList.count(); i++) {
				queryStr += " AND m.dmRecipient LIKE "
				    "'%'||:dmRecipient" + QString::number(i) +
				    "||'%'";
			}
		}

		if (!envel.dmSenderRefNumber().isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dmSenderRefNumber LIKE "
			    "'%'||:dmSenderRefNumber||'%'";
		}

		if (!envel.dmRecipientRefNumber().isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dmRecipientRefNumber LIKE "
			    "'%'||:dmRecipientRefNumber||'%'";
		}

		if (!envel.dmSenderIdent().isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dmSenderIdent LIKE "
			    "'%'||:dmSenderIdent||'%'";
		}

		if (!envel.dmRecipientIdent().isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dmRecipientIdent LIKE "
			    "'%'||:dmRecipientIdent||'%'";
		}

		if (!dmAddressList.isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "(m.dmSenderAddress LIKE "
			    "'%'||:dmSenderAddress0||'%' OR "
			    "m.dmRecipientAddress LIKE "
			    "'%'||:dmRecipientAddress0||'%')";
			for (i = 1; i < dmAddressList.count(); i++) {
				queryStr += " AND (m.dmSenderAddress LIKE "
				    "'%'||:dmSenderAddress" +
				    QString::number(i) +
				    "||'%' OR m.dmRecipientAddress LIKE "
				    "'%'||:dmRecipientAddress" +
				    QString::number(i) + "||'%')";
			}
		}

		if (!dmAnnotationList.isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dmAnnotation LIKE "
			    "'%'||:dmAnnotation0||'%'";
			for (i = 1; i < dmAnnotationList.count(); i++) {
				queryStr += " AND m.dmAnnotation LIKE "
				    "'%'||:dmAnnotation" + QString::number(i) +
				    "||'%'";
			}
		}

		if (!dmToHandsList.isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "m.dmToHands LIKE "
			    "'%'||:dmToHands0||'%'";
			for (i = 1; i < dmToHandsList.count(); i++) {
				queryStr += " AND m.dmToHands LIKE "
				    "'%'||:dmToHands" + QString::number(i) +
				    "||'%'";
			}
		}

		if (!dmFileNameList.isEmpty()) {
			if (isNotFirst) {
				queryStr += logicalToken;
			}
			isNotFirst = true;
			queryStr += "f._dmFileDescr LIKE "
			    "'%'||:_dmFileDescr0||'%'";
			for (i = 1; i < dmFileNameList.count(); i++) {
				queryStr += " AND f._dmFileDescr LIKE "
				    "'%'||:_dmFileDescr" + QString::number(i) +
				    "||'%'";
			}
		}

		/* prepare query string */
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return msgList;
		}

		/* query string binding */
		query.bindValue(":dbIDSender", envel.dbIDSender());
		query.bindValue(":dbIDRecipient", envel.dbIDRecipient());
		query.bindValue(":dmSenderRefNumber",
		    envel.dmSenderRefNumber());
		query.bindValue(":dmSenderIdent", envel.dmSenderIdent());
		query.bindValue(":dmRecipientRefNumber",
		    envel.dmRecipientRefNumber());
		query.bindValue(":dmRecipientIdent",
		    envel.dmRecipientIdent());

		if (!dmAddressList.isEmpty()) {
			for (i = 0; i < dmAddressList.count(); i++) {
				query.bindValue(":dmSenderAddress" +
				    QString::number(i),
				    dmAddressList[i]);
			}
			for (i = 0; i < dmAddressList.count(); i++) {
				query.bindValue(":dmRecipientAddress" +
				    QString::number(i),
				    dmAddressList[i]);
			}
		}

		if (!dmSenderList.isEmpty()) {
			for (i = 0; i < dmSenderList.count(); i++) {
				query.bindValue(":dmSender" +
				    QString::number(i),
				    dmSenderList[i]);
			}
		}

		if (!dmRecipientList.isEmpty()) {
			for (i = 0; i < dmRecipientList.count(); i++) {
				query.bindValue(":dmRecipient" +
				    QString::number(i),
				    dmRecipientList[i]);
			}
		}

		if (!dmToHandsList.isEmpty()) {
			for (i = 0; i < dmToHandsList.count(); i++) {
				query.bindValue(":dmToHands" +
				    QString::number(i),
				    dmToHandsList[i]);
			}
		}

		if (!dmAnnotationList.isEmpty()) {
			for (i = 0; i < dmAnnotationList.count(); i++) {
				query.bindValue(":dmAnnotation" +
				    QString::number(i),
				    dmAnnotationList[i]);
			}
		}

		if (!dmFileNameList.isEmpty()) {
			for (i = 0; i < dmFileNameList.count(); i++) {
				query.bindValue(":_dmFileDescr" +
				    QString::number(i),
				    dmFileNameList[i]);
			}
		}

		if (isMultiSelect) {
			if (MSG_RECEIVED == msgDirect) {
				query.bindValue(":message_type", TYPE_RECEIVED);
			} else {
				query.bindValue(":message_type", TYPE_SENT);
			}
		}
	} else {
		if (isMultiSelect) {
			queryStr += "s.message_type = :message_type";
			queryStr += logicalToken;
		}

		queryStr += "m.dmID LIKE '%'||:dmId||'%'";

		/* prepare query string */
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			query.lastError().text().toUtf8().constData());
			return msgList;
		}

		query.bindValue(":dmId", envel.dmId());

		if (isMultiSelect) {
			if (MSG_RECEIVED == msgDirect) {
				query.bindValue(":message_type", TYPE_RECEIVED);
			} else {
				query.bindValue(":message_type", TYPE_SENT);
			}
		}
	}

	if (query.exec() && query.isActive()) {
		if (query.first() && query.isValid()) {
			while (query.isValid()) {
				Q_ASSERT(8 == query.record().count());

				SoughtMsg foundMsgData(
				    query.value(0).toLongLong(),
				    dateTimeFromDbFormat(query.value(1).toString()),
				    int2messageType(query.value(5).toInt()),
				    query.value(2).toString(),
				    query.value(3).toString(),
				    query.value(4).toString(),
				    dateTimeFromDbFormat(query.value(6).toString()),
				    query.value(7).toBool());

				/*
				 * Cannot use isValid() because the dates may be
				 * missing.
				 */
				Q_ASSERT(foundMsgData.mId.dmId() >= 0);

				msgList.append(foundMsgData);
				query.next();
			}
		} else {
			/*
			 * SQL query can return empty search result.
			 * It is not an error. Show log information as warning.
			*/
			logWarningNL("Empty SQL search result: %s.",
			    query.lastError().text().toUtf8().constData());
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}

	return msgList;
}

MessageDb::SoughtMsg MessageDb::msgsGetMsgDataFromId(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT "
	    "m.dmID, m.dmDeliveryTime, "
	    "m.dmAnnotation, m.dmSender, m.dmRecipient, "
	    "s.message_type, m.dmAcceptanceTime, "
	    "(ifnull(r.message_id, 0) != 0) AS is_downloaded "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "LEFT JOIN raw_message_data AS r "
	    "ON (m.dmId = r.message_id) "
	    "WHERE m.dmID = :dmID";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmID", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			return SoughtMsg(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString()),
			    int2messageType(query.value(5).toInt()),
			    query.value(2).toString(),
			    query.value(3).toString(),
			    query.value(4).toString(),
			    dateTimeFromDbFormat(query.value(6).toString()),
			    query.value(7).toBool());
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return SoughtMsg();
}

bool MessageDb::msgsUpdateMessageState(qint64 dmId,
    const QDateTime &dmDeliveryTime, const QDateTime &dmAcceptanceTime,
    enum Isds::Type::DmState dmMessageStatus)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "UPDATE messages SET "
	    "dmDeliveryTime = :dmDeliveryTime, "
	    "dmAcceptanceTime = :dmAcceptanceTime, "
	    "dmMessageStatus = :dmMessageStatus "
	    "WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	query.bindValue(":dmDeliveryTime", nullVariantWhenIsNull(qDateTimeToDbFormat(dmDeliveryTime)));
	query.bindValue(":dmAcceptanceTime", nullVariantWhenIsNull(qDateTimeToDbFormat(dmAcceptanceTime)));
	query.bindValue(":dmMessageStatus", Isds::dmState2Variant(dmMessageStatus));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

bool MessageDb::deleteMessageAttachments(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "DELETE FROM files WHERE message_id = :message_id";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

bool MessageDb::insertOrUpdateMessageEvent(qint64 dmId,
    const Isds::Event &event)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	int eventId = -1;
	QString queryStr = "SELECT id FROM events WHERE "
	    "message_id = :message_id AND dmEventTime = :dmEventTime";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	query.bindValue(":dmEventTime", qDateTimeToDbFormat(event.time()));
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			eventId = query.value(0).toInt();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (-1 != eventId) {
		queryStr = "UPDATE events SET "
		"dmEventTime = :dmEventTime, dmEventDescr = :dmEventDescr "
		"WHERE id = :eventId";
	} else {
		queryStr = "INSERT INTO events (message_id, dmEventTime, "
		    "dmEventDescr) VALUES (:dmId, :dmEventTime, "
		    ":dmEventDescr)";
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	query.bindValue(":dmId", dmId);
	query.bindValue(":dmEventTime", nullVariantWhenIsNull(qDateTimeToDbFormat(event.time())));
	query.bindValue(":dmEventDescr", Isds::Event::type2string(event.type())
	    + QLatin1String(": ") + event.descr());
	if (-1 != eventId) {
		query.bindValue(":eventId", eventId);
	}
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Add/update message certificate in database.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @param[in] crtBase64 Base64-encoded certificate.
 * @return True on success.
 */
static
bool msgsInsertUpdateMessageCertBase64(QSqlQuery &query, qint64 dmId,
    const QByteArray &crtBase64)
{
	int certId = -1;
	bool certEntryFound = false;

	/* Search for certificate in 'certificate_data' table. */
	QString queryStr = "SELECT id FROM certificate_data WHERE "
	    "der_data = :der_data";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":der_data", nullVariantWhenIsNull(crtBase64));
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			certId = query.value(0).toInt(); /* Found. */
		} else {
			certId = -1; /* Not found. */
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* If certificate was not found. */
	if (-1 == certId) {
		/* Create unique certificate identifier. */
		queryStr = "SELECT max(id) FROM certificate_data";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		if (query.exec() && query.isActive()) {
			query.first();
			if (query.isValid()) {
				certId = query.value(0).toInt();
			} else {
				certId = -1;
			}
		} else {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		if (-1 == certId) {
			certId = 1; /* First certificate. */
		} else {
			++certId;
		}

		/* Insert missing certificate. */
		queryStr = "INSERT INTO certificate_data "
		    "(id, der_data) VALUES (:id, :der_data)";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":id", certId);
		query.bindValue(":der_data", nullVariantWhenIsNull(crtBase64));
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
	}

	/*
	 * Abort operation if there is still no matching certificate available.
	 */
	if (-1 == certId) {
		return false;
	}

	/*
	 * Tie certificate to message. Find whether there is already
	 * an entry for the message.
	 */
	queryStr = "SELECT * FROM message_certificate_data WHERE"
	    " message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		certEntryFound = query.isValid();
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/*
	 * Create or update message entry depending on whether a corresponding
	 * entry was found.
	 */
	if (certEntryFound) {
		queryStr = "UPDATE message_certificate_data SET "
		    "certificate_id = :certificate_id WHERE "
		    "message_id = :message_id";
	} else {
		queryStr = "INSERT INTO message_certificate_data "
		    "(message_id, certificate_id) VALUES "
		    "(:message_id, :certificate_id)";
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	query.bindValue(":certificate_id", certId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

/*!
 * @brief Insert/update raw (DER) message data into raw_message_data table.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @param[in] raw Raw (non-base64 encoded) message data.
 * @param[in] messageType Message type.
 * @return True on success.
 */
static
bool insertOrReplaceCompleteRaw(QSqlQuery &query, qint64 dmId,
    const QByteArray &raw, int messageType)
{
	struct x509_crt *crt = NULL;
	QString queryStr = "INSERT OR REPLACE INTO raw_message_data "
	    "(message_id, message_type, data) "
	    "VALUES (:dmId, :message_type, :data)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	query.bindValue(":data", (!raw.isNull()) ? raw.toBase64() : QVariant());
	query.bindValue(":message_type", messageType);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Get certificate data. */
	crt = raw_cms_signing_cert(raw.data(), raw.size());
	if (NULL != crt) {
		QByteArray crtDer;
		void *der = NULL;
		size_t derSize = 0;
		if (0 == x509_crt_to_der(crt, &der, &derSize)) {
			/* Method setRawData() does not copy the data! */
			crtDer.setRawData((char *) der, derSize);
			msgsInsertUpdateMessageCertBase64(query, dmId,
			    (!crtDer.isNull()) ? crtDer.toBase64() : QByteArray());
			::std::free(der); der = NULL; derSize = 0;
		}
		x509_crt_destroy(crt); crt = NULL;
	}
	return true;
fail:
	return false;
}

/*!
 * @brief Set the verification result.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @param[in] verified True is message was successfully verified,
 *                     false if verification failed.
 * @return True if update was successful.
 */
static
bool setVerified(QSqlQuery &query, qint64 dmId, bool verified)
{
	QString queryStr = "UPDATE messages SET is_verified = :verified "
	    "WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":verified", verified);
	query.bindValue(":dmId", dmId);
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Insert/update message hash into hashes table.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @param[in] hash Hash structure.
 * @return True on success.
 */
static
bool insertOrUpdateHash(QSqlQuery &query, qint64 dmId, const Isds::Hash &hash)
{
	int hashId= -1;
	QString queryStr = "SELECT id FROM hashes WHERE "
	    "message_id = :message_id";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			hashId = query.value(0).toInt();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (-1 != hashId) {
		queryStr = "UPDATE hashes SET value = :value, "
		    "_algorithm = :algorithm WHERE id = :hashId";
	} else {
		queryStr = "INSERT INTO hashes (message_id, value, _algorithm)"
		    " VALUES (:dmId, :value, :algorithm)";
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	query.bindValue(":value", nullVariantWhenIsNull(hash.base64Value()));
	query.bindValue(":algorithm", Isds::hashAlg2Variant(hash.algorithm()));
	if (-1 != hashId) {
		query.bindValue(":hashId", hashId);
	}
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Insert/update message file into files table.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @param[in] document Document.
 * @return True on success.
 */
static
bool insertOrUpdateAttachment(QSqlQuery &query, qint64 dmId,
    const Isds::Document &document)
{
	int fileId = -1;
	QString queryStr = "SELECT id FROM files WHERE "
	    "message_id = :message_id AND _dmFileDescr = :dmFileDescr AND "
	    "_dmMimeType = :dmMimeType AND "
	    "dmEncodedContent = :dmEncodedContent";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	query.bindValue(":dmFileDescr", document.fileDescr());
	query.bindValue(":dmMimeType", document.mimeType());
	query.bindValue(":dmEncodedContent", document.base64Content());
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			fileId = query.value(0).toInt();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (-1 != fileId) {
		queryStr = "UPDATE files SET "
		    " _dmFileDescr = :_dmFileDescr, "
		    "_dmUpFileGuid = :_dmUpFileGuid,"
		    " _dmFileGuid = :_dmFileGuid, _dmMimeType = :_dmMimeType, "
		    "_dmFormat = :_dmFormat, "
		    "_dmFileMetaType = :_dmFileMetaType, "
		    "dmEncodedContent = :dmEncodedContent "
		    "WHERE id = :fileId";
	} else {
		queryStr = "INSERT INTO files "
		    "(message_id, _dmFileDescr, _dmUpFileGuid, _dmFileGuid, "
		    "_dmMimeType, _dmFormat, _dmFileMetaType, dmEncodedContent"
		    ") VALUES ("
		    ":message_id, :_dmFileDescr, :_dmUpFileGuid, :_dmFileGuid,"
		    " :_dmMimeType, :_dmFormat, :_dmFileMetaType, "
		    ":dmEncodedContent)";
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	query.bindValue(":_dmFileDescr", nullVariantWhenIsNull(document.fileDescr()));
	query.bindValue(":_dmUpFileGuid", nullVariantWhenIsNull(document.upFileGuid()));
	query.bindValue(":_dmFileGuid", nullVariantWhenIsNull(document.fileGuid()));
	query.bindValue(":_dmMimeType", nullVariantWhenIsNull(document.mimeType()));
	query.bindValue(":_dmFormat", nullVariantWhenIsNull(document.format()));
	query.bindValue(":_dmFileMetaType", nullVariantWhenIsNull(fileMetaType2DbStr(document.fileMetaType())));
	query.bindValue(":dmEncodedContent", nullVariantWhenIsNull(document.base64Content()));
	if (-1 != fileId) {
		query.bindValue(":fileId", fileId);
	}
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Insert/update message files into files table.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @param[in] documents List of documents.
 * @return True on success.
 */
static
bool insertOrUpdateAttachments(QSqlQuery &query, qint64 dmId,
    const QList<Isds::Document> &documents)
{
	bool success = true;

	foreach (const Isds::Document &document, documents) {
		if (insertOrUpdateAttachment(query, dmId, document)) {
			logDebugLv0NL(
			    "Attachment file '%s' of message '%" PRId64 "' has been updated.",
			    document.fileDescr().toUtf8().constData(),
			    UGLY_QINT64_CAST dmId);
		} else {
			success = false;
			logErrorNL(
			    "Updating attachment file '%s' of message '%" PRId64 "' failed.",
			    document.fileDescr().toUtf8().constData(),
			    UGLY_QINT64_CAST dmId);
		}
	}

	return success;
}

bool MessageDb::insertOrReplaceCompleteMessage(const Isds::Message &msg,
    enum MessageDirection msgDirect, bool verified, bool transaction)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	qint64 dmId = msg.envelope().dmId();
	if (Q_UNLIKELY(msg.raw().isEmpty())) {
		logErrorNL("Missing raw data for message '%" PRId64 "'.",
		    UGLY_QINT64_CAST dmId);
		return false;
	}

	if (transaction) {
		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			logErrorNL("%s", "Cannot begin transaction.");
			goto fail;
		}
	}

	if (insertOrReplaceCompleteRaw(query, dmId, msg.raw(), 0)) {
		logDebugLv0NL(
		    "Raw data of message '%" PRId64 "' have been updated.",
		    UGLY_QINT64_CAST dmId);
	} else {
		logErrorNL("Updating raw data of message '%" PRId64 "' failed.",
		    UGLY_QINT64_CAST dmId);
		goto fail;
	}

	if (updateEnvelope(query, msg.envelope(), "tReturnedMessage",
	        msgDirect)) {
		logDebugLv0NL(
		    "Envelope of message '%" PRId64 "' has been updated.",
		    UGLY_QINT64_CAST dmId);
	} else {
		logErrorNL("Updating envelope of message '%" PRId64 "' failed.",
		    UGLY_QINT64_CAST dmId);
		goto fail;
	}

	if (Q_UNLIKELY(!setVerified(query, dmId, verified))) {
		goto fail;
	}

	if (insertOrUpdateHash(query, dmId, msg.envelope().dmHash())) {
		logDebugLv0NL("Hash of message '%" PRId64 "' has been updated.",
		    UGLY_QINT64_CAST dmId);
	} else {
		logErrorNL("Updating hash of message '%" PRId64 "' failed.",
		    UGLY_QINT64_CAST dmId);
		goto fail;
	}

	if (Q_UNLIKELY(!insertOrUpdateAttachments(query, dmId,
	        msg.documents()))) {
		goto fail;
	}

	if (transaction) {
		commitTransaction();
	}

	emit messageInserted(MsgId(dmId, msg.envelope().dmDeliveryTime()),
	    msgDirect);

	return true;

 fail:
	if (transaction) {
		rollbackTransaction();
	}
	return false;
}

bool MessageDb::insertOrUpdateMessageAttachments(qint64 dmId,
    const QList<Isds::Document> &documents, bool transaction)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	if (transaction) {
		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			logErrorNL("%s", "Cannot begin transaction.");
			return false;
		}
	}

	bool ret = insertOrUpdateAttachments(query, dmId, documents);
	if (Q_UNLIKELY(!ret)) {
		goto fail;
	}

	if (transaction) {
		commitTransaction();
	}
	return true;

fail:
	if (transaction) {
		rollbackTransaction();
	}
	return false;
}

QByteArray MessageDb::getCompleteMessageBase64(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT data FROM raw_message_data "
	    "WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toByteArray();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QByteArray();
}

bool MessageDb::isCompleteMessageInDb(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT count(*) FROM raw_message_data "
	    "WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toInt() != 0;
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

QByteArray MessageDb::getCompleteMessageRaw(qint64 dmId)
{
	return QByteArray::fromBase64(getCompleteMessageBase64(dmId));
}

QByteArray MessageDb::getDeliveryInfoBase64(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT data FROM raw_delivery_info_data "
	    "WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return query.value(0).toByteArray();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QByteArray();
}

QList<qint64> MessageDb::getAllMessageIDsWithoutAttach(void)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT dmID FROM messages AS m "
	    "LEFT JOIN raw_message_data AS r ON (m.dmID = r.message_id) "
	    "WHERE r.message_id IS null";

	QList<qint64> msgIdList;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			msgIdList.append(query.value(0).toLongLong());
			query.next();
		}
	}
	return msgIdList;
fail:
	return QList<qint64>();
}

QList<qint64> MessageDb::getAllMessageIDs(enum MessageType messageType)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QList<qint64> msgIdList;
	QString queryStr = "SELECT dmID FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE s.message_type = :message_type";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", messageType);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			msgIdList.append(query.value(0).toLongLong());
			query.next();
		}
		return msgIdList;
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QList<qint64>();
}

QSet<MsgId> MessageDb::getAllMsgIds(void)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QSet<MsgId> msgIdList;
	QString queryStr = "SELECT dmID, dmDeliveryTime FROM messages";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			msgIdList.insert(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return msgIdList;
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QSet<MsgId>();
}

QSet<MsgId> MessageDb::getAllMsgIds(enum MessageType messageType)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QSet<MsgId> msgIdSet;
	QString queryStr = "SELECT dmID, dmDeliveryTime FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "WHERE s.message_type = :message_type";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", messageType);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			msgIdSet.insert(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return msgIdSet;
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QSet<MsgId>();
}

QList<qint64> MessageDb::getAllMessageIDsEqualWithYear(const QString &year)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QList<qint64> msgList;
	QString queryStr;

	if (year == MessageDb::invalidYearName) {
		queryStr = "SELECT dmID FROM messages WHERE "
		    "ifnull(dmDeliveryTime, '') = ''";
	} else {
		queryStr = "SELECT dmID FROM messages WHERE "
		    "strftime('%Y', dmDeliveryTime) = '"+ year + "'";
	}

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			if (!query.value(0).toString().isEmpty()) {
				msgList.append(query.value(0).toLongLong());
			}
			query.next();
		}
		return msgList;
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QList<qint64>();
}

bool MessageDb::copyRelevantMsgsToNewDb(const QString &newDbFileName,
    const QString &year, bool transaction)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	bool attached = false;
	QString queryStr;

	QList<qint64> idList(getAllMessageIDsEqualWithYear(year));

	attached = attachDb2(query, newDbFileName);
	if (Q_UNLIKELY(!attached)) {
		goto fail;
	}

	// copy message data from messages table into new db.
	if (year == MessageDb::invalidYearName) {
		queryStr = "INSERT INTO " DB2 ".messages SELECT * FROM messages "
		    "WHERE ifnull(dmDeliveryTime, '') = ''";
	} else {
		queryStr = "INSERT INTO " DB2 ".messages SELECT * FROM messages "
		    "WHERE strftime('%Y', dmDeliveryTime) = '"+ year + "'";
	}
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (transaction) {
		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			logErrorNL("%s", "Cannot begin transaction.");
			goto fail;
		}
	}

	// copy other message data from other tables into new db.
	foreach (qint64 dmId, idList) {

		queryStr = "INSERT INTO " DB2 ".files SELECT * FROM files WHERE "
		    "message_id = :message_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_id", dmId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = "INSERT INTO " DB2 ".hashes SELECT * FROM hashes WHERE "
		    "message_id = :message_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_id", dmId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = "INSERT INTO " DB2 ".events SELECT * FROM events WHERE "
		    "message_id = :message_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_id", dmId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = "INSERT INTO " DB2 ".raw_message_data SELECT * "
		    "FROM raw_message_data WHERE message_id = :message_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_id", dmId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = "INSERT INTO " DB2 ".raw_delivery_info_data SELECT * "
		    "FROM raw_delivery_info_data WHERE message_id = :message_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_id", dmId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = "INSERT INTO " DB2 ".supplementary_message_data "
		    "SELECT * FROM supplementary_message_data WHERE "
		    "message_id = :message_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_id", dmId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = "INSERT INTO " DB2 ".process_state SELECT * FROM "
		    "process_state WHERE message_id = :message_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_id", dmId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = "INSERT OR REPLACE INTO " DB2 ".certificate_data SELECT * FROM "
		    "certificate_data";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}

		queryStr = "INSERT INTO " DB2 ".message_certificate_data SELECT * "
		    "FROM message_certificate_data WHERE "
		    "message_id = :message_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":message_id", dmId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
	}

	if (transaction) {
		commitTransaction();
	}
	detachDb2(query);

	return true;

fail:
	if (transaction) {
		rollbackTransaction();
	}
	if (attached) {
		detachDb2(query);
	}
	return false;
}

bool MessageDb::insertOrReplaceDeliveryInfoRaw(qint64 dmId,
    const QByteArray &raw)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "INSERT OR REPLACE INTO raw_delivery_info_data "
	    "(message_id, data) VALUES (:dmId, :data)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	query.bindValue(":data", (!raw.isNull()) ? raw.toBase64() : QVariant());
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

bool MessageDb::updateMessageAuthorInfo(qint64 dmId,
    enum Isds::Type::SenderType senderType, const QString &senderName)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QJsonObject authorObject;
	authorObject.insert("userType", (senderType != Isds::Type::ST_NULL) ?
	    Isds::senderType2Str(senderType) : QJsonValue(QJsonValue::Null));
	authorObject.insert("authorName", senderName.isEmpty() ?
	    QJsonValue(QJsonValue::Null) : senderName);
	QJsonObject object;
	object.insert("message_author", authorObject);

	QJsonDocument document;
	document.setObject(object);
	QString json = document.toJson(QJsonDocument::Compact);

	QString queryStr = "UPDATE supplementary_message_data SET "
	    "custom_data = :custom_data WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	query.bindValue(":custom_data", nullVariantWhenIsNull(json));
	if (query.exec()) {
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

const Isds::Hash MessageDb::getMessageHash(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	Isds::Hash hash;

	QString queryStr = "SELECT value, _algorithm FROM hashes WHERE "
	    "message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			hash.setBase64Value(query.value(0).toString());
			hash.setAlgorithm(Isds::variant2HashAlg(query.value(1)));
			return hash;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return Isds::Hash();
}

bool MessageDb::msgsDeleteMessageData(qint64 dmId, bool transaction)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;

	int certificateId = -1;
	bool deleteCert = false;
	bool emitAtEnd = false;

	MsgId msgId(dmId, QDateTime());

	if (transaction) {
		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			logErrorNL("%s", "Cannot begin transaction.");
			goto fail;
		}
	}

	/* Obtain full message identifier before deletion. */
	{
		/* Also get number of entries in the messages table. */
		queryStr = "SELECT COUNT(*), dmDeliveryTime FROM messages "
		    "WHERE dmID = :dmId";

		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":dmId", dmId);
		if (query.exec()) {
			if (query.isActive() && query.first() && query.isValid()) {
				emitAtEnd = (query.value(0).toInt() > 0);
				msgId.setDeliveryTime(
				    dateTimeFromDbFormat(query.value(1).toString()));
			} else {
				logWarningNL(
				    "Cannot obtain delivery time for message '%" PRId64 "'",
				    UGLY_QINT64_CAST dmId);
			}
		} else {
			logErrorNL(
			    "Cannot execute SQL query and/or read SQL data: %s.",
			    query.lastError().text().toUtf8().constData());
		}
	}

	/* Delete hash from hashes table. */
	queryStr = "DELETE FROM hashes WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Delete file(s) from files table. */
	queryStr = "DELETE FROM files WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Delete event(s) from events table. */
	queryStr = "DELETE FROM events WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Delete raw message data from raw_message_data table. */
	queryStr=
	    "DELETE FROM raw_message_data WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Delete raw info data from raw_delivery_info_data table. */
	queryStr = "DELETE FROM raw_delivery_info_data WHERE "
	    "message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Delete supplementary from supplementary_message_data table. */
	queryStr = "DELETE FROM supplementary_message_data WHERE "
	    "message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Select certificate_id from message_certificate_data table. */
	queryStr = "SELECT certificate_id FROM message_certificate_data WHERE "
	    "message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			certificateId = query.value(0).toInt();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	//Q_ASSERT(-1 != certificateId);

	/* Delete certificate reference from message_certificate_data table. */
	queryStr = "DELETE FROM message_certificate_data WHERE "
	    "message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Select certificate id from message_certificate_data table. */
	queryStr = "SELECT count(*) FROM message_certificate_data WHERE "
	    "certificate_id = :certificate_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":certificate_id", certificateId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			if (query.value(0).toInt() > 0) {
				deleteCert = false;
			} else {
				/* No message uses this certificate. */
				deleteCert = true;
			}
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/*
	 * Delete certificate data from certificate_data table if no messages.
	 */
	if (deleteCert) {
		queryStr = "DELETE FROM certificate_data WHERE "
		    "id = :certificate_id";
		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":certificate_id", certificateId);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
	}

	/* Delete process state information from process_state table. */
	queryStr = "DELETE FROM process_state WHERE "
	    "message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Delete message from messages table. */
	queryStr = "DELETE FROM messages WHERE dmID = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (transaction) {
		commitTransaction();
	}

	if (emitAtEnd) {
		emit messageDataDeleted(msgId);
	}

	return true;

fail:
	if (transaction) {
		rollbackTransaction();
	}
	return false;
}

QList<MsgId> MessageDb::msgsDateInterval(const QDate &fromDate,
    const QDate &toDate, enum MessageDirection msgDirect)
{
	QMutexLocker locker(&m_lock);
	/* TODO -- Check whether time is interpreted in correct time zone! */

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;
	QList<MsgId> dmIDs;

	queryStr = "SELECT dmID, dmDeliveryTime "
	    "FROM messages AS m LEFT JOIN supplementary_message_data "
	    "AS s ON (m.dmID = s.message_id) WHERE "
	    "message_type = :message_type AND "
	    "(strftime('%Y-%m-%d', m.dmDeliveryTime) >= :fromDate) AND "
	    "(strftime('%Y-%m-%d', m.dmDeliveryTime) <= :toDate)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (MSG_RECEIVED == msgDirect) {
		query.bindValue(":message_type", TYPE_RECEIVED);
	} else {
		query.bindValue(":message_type", TYPE_SENT);
	}
	query.bindValue(":fromDate", fromDate);
	query.bindValue(":toDate", toDate);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			dmIDs.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return dmIDs;

fail:
	return QList<MsgId>();
}

QStringList MessageDb::getMessageForHtmlExport(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QStringList messageItems;
	QString queryStr = "SELECT dmSender, dmRecipient, dmAnnotation, "
	    "dmDeliveryTime, dmAcceptanceTime "
	    "FROM messages WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		messageItems.append(query.value(0).toString());
		messageItems.append(query.value(1).toString());
		messageItems.append(query.value(2).toString());
		messageItems.append(
		    dateTimeFromDbFormat(query.value(3).toString())
		        .toString("dd.MM.yyyy hh:mm:ss"));
		messageItems.append(
		    dateTimeFromDbFormat(query.value(4).toString())
		        .toString("dd.MM.yyyy hh:mm:ss"));
		return messageItems;
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QStringList();
}

QStringList MessageDb::getMessageForCsvExport(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QStringList messageItems;
	QString queryStr = "SELECT dmMessageStatus, _dmType, dmDeliveryTime, "
	    "dmAcceptanceTime, dmAnnotation, dmSender, dmSenderAddress, "
	    "dmRecipient, dmRecipientAddress, dmSenderIdent, "
	    "dmSenderRefNumber, dmRecipientIdent, dmRecipientRefNumber "
	    "FROM messages WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		int count = query.record().count();
		Q_ASSERT(13 == count);
		for (int i = 0; i < count; ++i) {
			QString element = query.value(i).toString();
			if (element.contains(',')) {
				element = '"' + element + '"';
			}
			messageItems.append(element);
		}
		return messageItems;
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QStringList();
}

/*!
 * @brief Converts integer value to message processing state.
 *
 * @param[in]  val Integer value.
 * @param[out] ok Set to true on success.
 * @return Message processing state, UNSETTLED on error.
 */
static
enum MessageDb::MessageProcessState int2messageProcessState(int val,
    bool *ok = Q_NULLPTR)
{
	enum MessageDb::MessageProcessState state = MessageDb::UNSETTLED;

	switch (val) {
	case MessageDb::UNSETTLED:
		state = MessageDb::UNSETTLED;
		break;
	case MessageDb::IN_PROGRESS:
		state = MessageDb::IN_PROGRESS;
		break;
	case MessageDb::SETTLED:
		state = MessageDb::SETTLED;
		break;
	default:
		Q_ASSERT(0);
		if (ok != Q_NULLPTR) {
			*ok = false;
		}
		return state;
		break;
	}

	if (ok != Q_NULLPTR) {
		*ok = true;
	}
	return state;
}

/*!
 * @brief Get all messages from list not matching the process state.
 *
 * @param[in] query SQL query to work with.
 * @param[in] msgIds Message identifiers to be checked.
 * @param[in] state Message state.
 * @return Message identifier list not matching the read status.
 */
static
QList<MsgId> notSatisfyingProcessState(QSqlQuery &query,
    const QList<MsgId> &msgIds, enum MessageDb::MessageProcessState state)
{
	QString queryStr;
	{
		QString idListing = toListString(msgIds);
		if (Q_UNLIKELY(idListing.isEmpty())) {
			return QList<MsgId>();
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString("SELECT m.dmId, m.dmDeliveryTime "
		    "FROM messages AS m "
		    "LEFT JOIN process_state AS p ON (m.dmID = p.message_id) "
		    "WHERE (ifnull(p.state, 0) != :state) AND (m.dmID IN (%1))")
		        .arg(idListing);
	}

	QList<MsgId> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":state", (int) state);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<MsgId>();
}

bool MessageDb::setMessagesProcessState(const QList<MsgId> &msgIds,
    enum MessageProcessState state)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr;
	QList<MsgId> toBeChanged = notSatisfyingProcessState(query, msgIds,
	    state);
	{
		if (toBeChanged.isEmpty()) {
			/* Same values already stored. */
			return true;
		}

		QString idListing = toListString(toBeChanged);
		if (Q_UNLIKELY(idListing.isEmpty())) {
			Q_ASSERT(0);
			return false;
		}

		/* There is no way how to use query.bind() to enter list values. */
		queryStr = QString(
		    "INSERT OR REPLACE INTO process_state (message_id, state) "
		    "SELECT m.dmId, :state "
		    "FROM messages AS m "
		    "WHERE m.dmID IN (%1)").arg(idListing);
	}

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":state", (int) state);
	if (query.exec()) {
		emit changedProcessState(toBeChanged, state);
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

enum MessageDb::MessageProcessState MessageDb::getMessageProcessState(
    qint64 dmId, bool *ok)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr = "SELECT state FROM process_state "
	    "WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		return int2messageProcessState(query.value(0).toInt(), ok);
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	if (ok != Q_NULLPTR) {
		*ok = false;
	}
	return MessageDb::UNSETTLED;
}

/*!
 * @brief Get all messages not matching the process state.
 *
 * @param[in] query SQML query to work with.
 * @param[in] state Message state.
 * @return Message identifier list not matching the read status.
 */
static
QList<MsgId> allReceivedNotSatisfyingProcessState(QSqlQuery &query,
    enum MessageDb::MessageProcessState state)
{
	QString queryStr = "SELECT m.dmId, m.dmDeliveryTime "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s ON (m.dmID = s.message_id) "
	    "LEFT JOIN process_state AS p ON (m.dmID = p.message_id) "
	    "WHERE (ifnull(p.state, 0) != :state) AND (s.message_type = :message_type)";

	QList<MsgId> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":state", (int) state);
	query.bindValue(":message_type", MessageDb::TYPE_RECEIVED);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<MsgId>();
}

bool MessageDb::setReceivedMessagesProcessState(enum MessageProcessState state)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QList<MsgId> toBeChanged = allReceivedNotSatisfyingProcessState(query,
	    state);
	if (toBeChanged.isEmpty()) {
		/* Same values already stored. */
		return true;
	}

	QString queryStr =
	    "INSERT OR REPLACE INTO process_state (message_id, state)"
	    " SELECT s.message_id, :state "
	    "FROM supplementary_message_data AS s "
	    "WHERE s.message_type = :message_type";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":state", (int) state);
	query.bindValue(":message_type", TYPE_RECEIVED);
	if (query.exec()) {
		emit changedProcessState(toBeChanged, state);
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Get all messages not matching the process state.
 *
 * @param[in] query SQL query to work with.
 * @param[in] year Year number.
 * @param[in] state Message state.
 * @return Message identifier list not matching the read status.
 */
static
QList<MsgId> allReceivedYearNotSatisfyingProcessState(QSqlQuery &query,
    const QString &year, enum MessageDb::MessageProcessState state)
{
	QString queryStr = "SELECT m.dmId, m.dmDeliveryTime "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s ON (m.dmID = s.message_id) "
	    "LEFT JOIN process_state AS p ON (m.dmID = p.message_id) "
	    "WHERE (ifnull(p.state, 0) != :state) AND (s.message_type = :message_type) AND "
	    "(ifnull(strftime('%Y', m.dmDeliveryTime), '" INVALID_YEAR "') = :year)";

	QList<MsgId> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":state", (int) state);
	query.bindValue(":message_type", MessageDb::TYPE_RECEIVED);
	query.bindValue(":year", year);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<MsgId>();
}

bool MessageDb::smsgdtSetReceivedYearProcessState(const QString &year,
    enum MessageProcessState state)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QList<MsgId> toBeChanged = allReceivedYearNotSatisfyingProcessState(
	    query, year, state);
	if (toBeChanged.isEmpty()) {
		/* Same values already stored. */
		return true;
	}

	QString queryStr =
	    "INSERT OR REPLACE INTO process_state (message_id, state)"
	    " SELECT s.message_id, :state "
	    "FROM supplementary_message_data AS s "
	    "LEFT JOIN messages AS m ON (s.message_id = m.dmID) "
	    "WHERE (ifnull(strftime('%Y', m.dmDeliveryTime), "
	    "'" INVALID_YEAR "') = :year) and (s.message_type = :message_type)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":state", (int) state);
	query.bindValue(":year", nullVariantWhenIsNull(year));
	query.bindValue(":message_type", TYPE_RECEIVED);
	if (query.exec()) {
		emit changedProcessState(toBeChanged, state);
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

/*!
 * @brief Get all messages delivered within 90 days not matching the process state.
 *
 * @param[in] query SQL query to work with.
 * @param[in] state Message state.
 * @return Message identifier list not matching the read status.
 */
static
QList<MsgId> allReceivedYearNotSatisfyingProcessState(QSqlQuery &query,
    enum MessageDb::MessageProcessState state)
{
	QString queryStr = "SELECT m.dmId, m.dmDeliveryTime "
	    "FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s ON (m.dmID = s.message_id) "
	    "LEFT JOIN process_state AS p ON (m.dmID = p.message_id) "
	    "WHERE (ifnull(p.state, 0) != :state) AND (s.message_type = :message_type) AND "
	    "(m.dmDeliveryTime >= date('now','-90 day'))";

	QList<MsgId> result;

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":state", (int) state);
	query.bindValue(":message_type", MessageDb::TYPE_RECEIVED);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			result.append(MsgId(query.value(0).toLongLong(),
			    dateTimeFromDbFormat(query.value(1).toString())));
			query.next();
		}
		return result;
	}  else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
fail:
	return QList<MsgId>();
}

bool MessageDb::smsgdtSetWithin90DaysReceivedProcessState(
    enum MessageProcessState state)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QList<MsgId> toBeChanged = allReceivedYearNotSatisfyingProcessState(
	    query, state);
	if (toBeChanged.isEmpty()) {
		/* Same values already stored. */
		return true;
	}

	QString queryStr =
	    "INSERT OR REPLACE INTO process_state (message_id, state)"
	    " SELECT s.message_id, :state "
	    "FROM supplementary_message_data AS s "
	    "LEFT JOIN messages AS m ON (s.message_id = m.dmID) "
	    "WHERE (m.dmDeliveryTime >= date('now','-90 day')) and "
	    "(s.message_type = :message_type)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":state", (int) state);
	query.bindValue(":message_type", TYPE_RECEIVED);
	if (query.exec()) {
		emit changedProcessState(toBeChanged, state);
		return true;
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

QByteArray MessageDb::getMessageTimestampRaw(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT dmQTimestamp FROM messages "
	    "WHERE dmID = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		QByteArray byteArray = query.value(0).toByteArray();
		if (Q_UNLIKELY(byteArray.isEmpty())) {
			return QByteArray();
		}
		return QByteArray::fromBase64(byteArray);
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QByteArray();
}

bool MessageDb::msgsRcvdWithin90DaysQuery(QSqlQuery &query)
{
	QString queryStr = "SELECT ";
	for (int i = 0; i < (rcvdItemIds.size() - 2); ++i) {
		queryStr += rcvdItemIds[i] + ", ";
	}
	queryStr += "(ifnull(r.message_id, 0) != 0) AS is_downloaded" ", "
	    "ifnull(p.state, 0) AS process_status";
	queryStr += " FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "LEFT JOIN raw_message_data AS r "
	    "ON (m.dmId = r.message_id) "
	    "LEFT JOIN process_state AS p "
	    "ON (m.dmId = p.message_id) "
	    "WHERE "
	    "(s.message_type = :message_type)"
	    " and "
	    "(m.dmDeliveryTime >= date('now','-90 day'))";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", TYPE_RECEIVED);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

bool MessageDb::msgsSntWithin90DaysQuery(QSqlQuery &query)
{
	QString queryStr = "SELECT ";
	for (int i = 0; i < (sntItemIds.size() - 1); ++i) {
		queryStr += sntItemIds[i] + ", ";
	}
	queryStr += "(ifnull(r.message_id, 0) != 0) AS is_downloaded";
	queryStr += " FROM messages AS m "
	    "LEFT JOIN supplementary_message_data AS s "
	    "ON (m.dmID = s.message_id) "
	    "LEFT JOIN raw_message_data AS r "
	    "ON (m.dmId = r.message_id) "
	    "WHERE "
	    "(s.message_type = :message_type)"
	    " and "
	    "(m.dmDeliveryTime >= date('now','-90 day'))";
//	    "((m.dmDeliveryTime >= date('now','-90 day')) or "
//	    " (m.dmDeliveryTime IS NULL))";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_type", TYPE_SENT);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

QList<class SQLiteTbl *> MessageDb::listOfTables(void) const
{
	QList<class SQLiteTbl *> tables;
	tables.append(&msgsTbl);
	tables.append(&flsTbl);
	tables.append(&hshsTbl);
	tables.append(&evntsTbl);
	tables.append(&prcstTbl);
	tables.append(&rwmsgdtTbl);
	tables.append(&rwdlvrinfdtTbl);
	tables.append(&smsgdtTbl);
	tables.append(&crtdtTbl);
	tables.append(&msgcrtdtTbl);
	return tables;
}

/*!
 * @brief This method ensures that the process_state table
 *     contains a PRIMARY KEY. This table might be created without any
 *     primary key reference due to a bug in a previous version.
 *
 * @return True on success.
 *
 * TODO -- This method may be removed in some future version
 *     of the programme.
 */
static
bool ensurePrimaryKeyInProcessStateTable(const QSqlDatabase &db)
{
	QSqlQuery query(db);
	QString queryStr;
	QString createTableSql;

	queryStr = "SELECT sql FROM sqlite_master "
	    "WHERE (type = :type) and (name = :name)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":type", QString("table"));
	query.bindValue(":name", QString("process_state"));
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		createTableSql = query.value(0).toString();
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (Q_UNLIKELY(createTableSql.isEmpty())) {
		goto fail;
	}


	if (createTableSql.contains("PRIMARY", Qt::CaseSensitive)) {
		return true;
	}

	/* Table does not contain primary key. */

	/* Rename existing table. */
	queryStr = "ALTER TABLE process_state RENAME TO _process_state";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (Q_UNLIKELY(!prcstTbl.createEmpty(db))) {
		goto fail;
	}

	/* Copy table content. */
	queryStr = "INSERT OR REPLACE INTO process_state (message_id, state) "
	    "SELECT message_id, state FROM _process_state";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Delete old table. */
	queryStr = "DROP TABLE _process_state";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	return true;

fail:
	return false;
}

bool MessageDb::assureConsistency(void)
{
	logInfoNL(
	    "Assuring primary key in process_state table in database '%s'.",
	    fileName().toUtf8().constData());

	QMutexLocker locker(&m_lock);

	return ensurePrimaryKeyInProcessStateTable(
	    DelayedAccessSQLiteDb::accessDb());
}

QDateTime MessageDb::msgsVerificationDate(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;

	if (PrefsSpecific::DOWNLOAD_DATE ==
	    PrefsSpecific::certValDate(*GlobInstcs::prefsPtr)) {

		queryStr = "SELECT download_date "
		    "FROM supplementary_message_data WHERE message_id = :dmId";

		if (Q_UNLIKELY(!query.prepare(queryStr))) {
			logErrorNL("Cannot prepare SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
		query.bindValue(":dmId", dmId);
		if (query.exec() && query.isActive() &&
		    query.first() && query.isValid()) {
			QDateTime dateTime =
			    dateTimeFromDbFormat(query.value(0).toString());
			if (dateTime.isValid()) {
				return dateTime;
			}
		} else {
			logErrorNL(
			    "Cannot execute SQL query and/or read SQL data: %s.",
			    query.lastError().text().toUtf8().constData());
			goto fail;
		}
	}
	return QDateTime::currentDateTime();
fail:
	return QDateTime();
}

MessageDb::FilenameEntry MessageDb::msgsGetAdditionalFilenameEntry(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;
	FilenameEntry entry;

	queryStr = "SELECT dmDeliveryTime, dmAcceptanceTime, dmAnnotation, "
	    "dmSender, dmRecipient FROM messages WHERE dmId = :dmId";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		entry.dmDeliveryTime =
		    dateTimeFromDbFormat(query.value(0).toString());
		entry.dmAcceptanceTime =
		    dateTimeFromDbFormat(query.value(1).toString());
		entry.dmAnnotation = query.value(2).toString();
		entry.dmSender = query.value(3).toString();
		entry.dmRecipient = query.value(4).toString();
		return entry;
	} else {
		logErrorNL("Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return FilenameEntry();
}

QJsonDocument MessageDb::getMessageCustomData(qint64 dmId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr = "SELECT custom_data FROM supplementary_message_data "
	    "WHERE message_id = :dmId";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmId", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			return QJsonDocument::fromJson(
			    query.value(0).toByteArray());
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return QJsonDocument();
}

bool MessageDb::msgCertValidAtDate(qint64 dmId, const QDateTime &dateTime,
    bool ignoreMissingCrlCheck)
{
	debugFuncCall();

	QByteArray rawBytes = getCompleteMessageRaw(dmId);
	Q_ASSERT(rawBytes.size() > 0);

	if (ignoreMissingCrlCheck) {
		logWarningNL(
		    "CRL check is not performed for message '%" PRId64 "'.",
		    UGLY_QINT64_CAST dmId);
	}
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
	int64_t utcTime = dateTime.toSecsSinceEpoch();
#else /* < Qt-5.8 */
	int64_t utcTime = dateTime.toMSecsSinceEpoch() / 1000;
#endif /* >= Qt-5.8 */

	return 1 == raw_msg_verify_signature_date(
	    rawBytes.data(), rawBytes.size(), utcTime,
	    ignoreMissingCrlCheck ? 0 : 1);
}

bool MessageDb::isRelevantMsgForImport(qint64 dmId, const QString &databoxId)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QString queryStr;

	queryStr = "SELECT dmID FROM messages WHERE dmID = :dmID AND "
	    "(dbIDSender = :dbIDSender OR dbIDRecipient = :dbIDRecipient)";

	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmID", dmId);
	query.bindValue(":dbIDSender", databoxId);
	query.bindValue(":dbIDRecipient", databoxId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			return !query.value(0).toString().isEmpty();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
	}
fail:
	return false;
}

bool MessageDb::copyCompleteMsgDataToAccountDb(const QString &sourceDbPath,
    qint64 dmId, bool transaction)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QByteArray der_data;
	bool attached = false;
	QString queryStr;

	attached = attachDb2(query, sourceDbPath);
	if (Q_UNLIKELY(!attached)) {
		goto fail;
	}

	if (transaction) {
		transaction = beginTransaction();
		if (Q_UNLIKELY(!transaction)) {
			logErrorNL("%s", "Cannot begin transaction.");
			goto fail;
		}
	}

	// copy message envelope data from messages table into db.
	queryStr = "INSERT INTO messages SELECT * FROM " DB2 ".messages WHERE "
	    "dmID = :dmID";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":dmID", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	// copy other message data from other tables into new db.
	// FILES - insert all columns without id.
	queryStr = "INSERT INTO files "
	    "(message_id, _dmFileDescr, _dmUpFileGuid, _dmFileGuid, "
	    "_dmMimeType, _dmFormat, _dmFileMetaType, dmEncodedContent) "
	    "SELECT "
	    "message_id, _dmFileDescr, _dmUpFileGuid, _dmFileGuid, "
	    "_dmMimeType, _dmFormat, _dmFileMetaType, dmEncodedContent "
	    "FROM " DB2 ".files WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	// HASHES - insert all columns without id.
	queryStr = "INSERT INTO hashes "
	    "(message_id, value, _algorithm) "
	    "SELECT "
	    "message_id, value, _algorithm "
	    "FROM " DB2 ".hashes WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	// HASHES - insert all columns without id.
	queryStr = "INSERT INTO events "
	    "(message_id, dmEventTime, dmEventDescr) "
	    "SELECT "
	    "message_id, dmEventTime, dmEventDescr "
	    "FROM " DB2 ".events WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	// RAW_MESSAGE_DATA - insert all columns.
	queryStr = "INSERT INTO raw_message_data SELECT * "
	    "FROM " DB2 ".raw_message_data WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	// RAW_DELIVERY_INFO_DATA - insert all columns.
	queryStr = "INSERT INTO raw_delivery_info_data SELECT * "
	    "FROM " DB2 ".raw_delivery_info_data WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	// supplementary_message_data - insert all columns.
	queryStr = "INSERT INTO supplementary_message_data "
	    "SELECT * FROM " DB2 ".supplementary_message_data WHERE "
	    "message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	// process_state - insert all columns.
	queryStr = "INSERT INTO process_state SELECT * FROM " DB2
	    ".process_state WHERE message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* insert message_certificate_data */
	queryStr = "INSERT INTO message_certificate_data SELECT * "
	    "FROM " DB2 ".message_certificate_data WHERE "
	    "message_id = :message_id";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot exec SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* get cert der_data based to message_id from source database */
	queryStr = "SELECT der_data FROM " DB2 ".certificate_data WHERE id IN "
	    "(SELECT certificate_id FROM " DB2 ".message_certificate_data "
	    "WHERE message_id = :message_id)";
	if (Q_UNLIKELY(!query.prepare(queryStr))) {
		logErrorNL("Cannot prepare SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}
	query.bindValue(":message_id", dmId);
	if (query.exec() && query.isActive() &&
	    query.first() && query.isValid()) {
		der_data = query.value(0).toByteArray();
	} else {
		logInfoMl("Cannot exec SQL query - "
		    "message cert data missing: %s.",
		    query.lastError().text().toUtf8().constData());
	}

	if (!der_data.isEmpty()) {
		/*
		 * Check if der_data exist in the target database and update
		 * the message certificate_id.
		 */
		if (Q_UNLIKELY(!msgsInsertUpdateMessageCertBase64(query, dmId, der_data))) {
			goto fail;
		}
	}

	if (transaction) {
		commitTransaction();
	}
	/*
	 * Detach has to work directly on query here. Another query cannot be
	 * created because this locks the database for some reason.
	 */
	detachDb2(query);

	return true;

fail:
	if (transaction) {
		rollbackTransaction();
	}
	if (attached) {
		detachDb2(query);
	}
	return false;
}
