/*
 * Copyright (C) 2014-2024 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 <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QList>
#include <QMap>
#include <QMutexLocker>
#include <QPair>
#include <QRegularExpression>
#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/compiler.h" /* macroStdMove */
#include "src/datovka_shared/compat_qt/misc.h" /* _Qt_SkipEmptyParts, qsizetype */
#include "src/datovka_shared/compat_qt/variant.h" /* nullVariantWhenIsNull */
#include "src/datovka_shared/html/html_export.h"
#include "src/datovka_shared/isds/json_conversion.h"
#include "src/datovka_shared/isds/message_interface2.h"
#include "src/datovka_shared/isds/to_text_conversion.h"
#include "src/datovka_shared/isds/type_conversion.h"
#include "src/datovka_shared/isds/type_description.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/utility/date_time.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/io/filesystem.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", "attNum",
    "_dmFileDescr", "_dmMimeType", "dmEncodedContent"};
static
const QVector<QString> fileItemIdsNoDataSize = {"id", "attNum",
    "_dmFileDescr", "_dmMimeType", "null"};

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)
{
}

QString MessageDb::assocFileDirPath(void) const
{
	return _assocFileDirPath(fileName());
}

QString MessageDb::_assocFileDirPath(QString fileName)
{
	/* Remove trailing '.db' suffix from current database file path. */
	static const QRegularExpression re("\\.db$");

	if (SQLiteDb::memoryLocation == fileName) {
		/* Return empty string if database is held in memory. */
		return QString();
	}

	return fileName.replace(re, QString());
}

bool MessageDb::copyDb(const QString &newFileName, enum SQLiteDb::OpenFlag flag)
{
	{
		const QString newDirPath = QFileInfo(newFileName).absoluteDir().absolutePath();
		const QString oldDirPath = assocFileDirPath();
		if ((!oldDirPath.isEmpty()) && QFileInfo(oldDirPath).exists()) {
			bool ret = copyDirRecursively(oldDirPath, newDirPath, true);
			if (!ret) {
				return false;
			}
		}
	}

	return DelayedAccessSQLiteDb::copyDb(newFileName, flag);
}

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);
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	Isds::Envelope envelope;
	QList<Isds::Event> events;

	QString queryStr = "SELECT dmAnnotation, "
	    "dbIDSender, dmSender, dmSenderAddress, dmSenderType, "
	    "dmSenderOrgUnit, dmSenderOrgUnitNum, dmSenderRefNumber, "
	    "dmSenderIdent, dbIDRecipient, dmRecipient, dmRecipientAddress, "
	    "dmRecipientOrgUnit, dmRecipientOrgUnitNum, dmAmbiguousRecipient, "
	    "dmRecipientRefNumber, dmRecipientIdent, dmLegalTitleLaw, "
	    "dmLegalTitleYear, dmLegalTitleSect, dmLegalTitlePar, "
	    "dmLegalTitlePoint, dmToHands, dmPersonalDelivery, "
	    "dmAllowSubstDelivery, dmQTimestamp, dmDeliveryTime, "
	    "dmAcceptanceTime, dmMessageStatus, dmAttachmentSize, _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()) {
		envelope.setDmId(dmId);
		envelope.setDmID(QString::number(dmId));
		envelope.setDmAnnotation(query.value(0).toString());
		envelope.setDbIDSender(query.value(1).toString());
		envelope.setDmSender(query.value(2).toString());
		envelope.setDmSenderAddress(query.value(3).toString());
		envelope.setDmSenderType(
		    Isds::intVariant2DbType(query.value(4)));
		envelope.setDmSenderOrgUnit(query.value(5).toString());
		envelope.setDmSenderOrgUnitNumStr(query.value(6).toString());
		envelope.setDmSenderRefNumber(query.value(7).toString());
		envelope.setDmSenderIdent(query.value(8).toString());
		envelope.setDbIDRecipient(query.value(9).toString());
		envelope.setDmRecipient(query.value(10).toString());
		envelope.setDmRecipientAddress(query.value(11).toString());
		envelope.setDmRecipientOrgUnit(query.value(12).toString());
		envelope.setDmRecipientOrgUnitNumStr(
		    query.value(13).toString());
		envelope.setDmAmbiguousRecipient(
		    Isds::variant2NilBool(query.value(14)));
		envelope.setDmRecipientRefNumber(query.value(15).toString());
		envelope.setDmRecipientIdent(query.value(16).toString());
		envelope.setDmLegalTitleLawStr(query.value(17).toString());
		envelope.setDmLegalTitleYearStr(query.value(18).toString());
		envelope.setDmLegalTitleSect(query.value(19).toString());
		envelope.setDmLegalTitlePar(query.value(20).toString());
		envelope.setDmLegalTitlePoint(query.value(21).toString());
		envelope.setDmToHands(query.value(22).toString());
		envelope.setDmPersonalDelivery(
		     Isds::variant2NilBool(query.value(23)));
		envelope.setDmAllowSubstDelivery(
		    Isds::variant2NilBool(query.value(24)));
		envelope.setDmQTimestamp(query.value(25).toByteArray());
		envelope.setDmDeliveryTime(
		    dateTimeFromDbFormat(query.value(26).toString()));
		envelope.setDmAcceptanceTime(
		    dateTimeFromDbFormat(query.value(27).toString()));
		envelope.setDmMessageStatus(
		    Isds::variant2DmState(query.value(28)));
		envelope.setDmAttachmentSize(query.value(29).toLongLong());
		envelope.setDmType(Isds::variant2Char(query.value(30)));
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Events may be empty. */
	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());
	}
	query.bindValue(":dmID", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		while (query.isValid()) {
			Isds::Event event;
			event.setTime(dateTimeFromDbFormat(query.value(0).toString()));
			event.setDescr(query.value(1).toString());
			events.append(event);
			query.next();
		}
	} else {
		logErrorNL(
		    "Cannot execute SQL query and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
	}

	envelope.setDmEvents(events);
	return envelope;

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)
{
	QList<MsgId> toBeChanged;

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		QString queryStr;
		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());
			return false;
		}
		query.bindValue(":read", read);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	}

	/* Signal must not be emitted when write lock is active. */
	emit changedLocallyRead(toBeChanged, read);
	return true;
}

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

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString queryStr = "SELECT dmVODZ "
	    "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()) {
		if (ok != Q_NULLPTR) {
			*ok = true;
		}
		query.first();
		if (query.isValid()) {
			return query.value(0).toBool();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s",
		    query.lastError().text().toUtf8().constData());
	}

fail:
	if (ok != Q_NULLPTR) {
		*ok = false;
	}
	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)
{
	QList<MsgId> toBeChanged;

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		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());
			return false;
		}
		query.bindValue(":read", read);
		query.bindValue(":message_type", TYPE_RECEIVED);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	}

	/* Signal must not be emitted when write lock is active. */
	emit changedLocallyRead(toBeChanged, read);
	return true;
}

/*!
 * @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)
{
	QList<MsgId> toBeChanged;

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		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());
			return false;
		}
		query.bindValue(":read", read);
		query.bindValue(":year", nullVariantWhenIsNull(year));
		query.bindValue(":message_type", TYPE_RECEIVED);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	}

	/* Signal must not be emitted when write lock is active. */
	emit changedLocallyRead(toBeChanged, read);
	return true;
}

/*!
 * @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)
{
	QList<MsgId> toBeChanged;

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		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());
			return false;
		}
		query.bindValue(":read", read);
		query.bindValue(":message_type", TYPE_RECEIVED);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	}

	/* Signal must not be emitted when write lock is active. */
	emit changedLocallyRead(toBeChanged, read);
	return true;
}

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::verifySignatureHtml(qint64 dmId)
{
	QString sigResStr;
	QString certResStr;
	QString tsResStr;

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

			tsResStr = (1 == ret) ? tr("Valid") : tr("Invalid");
			if (-1 != ret) {
				tsResStr +=
				    " (" +
				    tst.toString(Utility::dateTimeDisplayFormat)
				    + " " +
				    tst.timeZone().abbreviation(tst) +
				    ")";
			}
		}

		if (tstData.isEmpty()) {
			tsResStr += tr("Download the complete message in order to verify its time stamp.");
		}
	}

	return Html::Export::htmlSignatureInfoApp(sigResStr, certResStr,
	    tsResStr);
}

/*
 * 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);
	}
}

/*!
 * @brief Create a path of a directory where to store data of message with given ID.
 *
 * @note The directory is not actually created.
 *
 * @param[in] assocFileDirPath General path for this database.
 * @param[in] dmId Message identifier.
 * @return String containing a directory path.
 */
static
QString constructDirPath(const QString &assocFileDirPath, qint64 dmId)
{
	if (Q_UNLIKELY(assocFileDirPath.isEmpty())) {
		return QString();
	}

	return assocFileDirPath + QStringLiteral("/") + QString::number(dmId);
}

/*!
 * @brief Create the file name for a data message file with given ID.
 *
 * @param[in] messageType Determines received or sent message.
 * @param[in] dmId Message identifier.
 * @return File name.
 */
static
QString msgZfoFileName(enum MessageDb::MessageType messageType, qint64 dmId)
{
	return (MessageDb::TYPE_RECEIVED == messageType) ?
	    QString("DDZ_%1.zfo").arg(dmId) : QString("ODZ_%1.zfo").arg(dmId);
}

/*!
 * @brief Create the file name for a delivery info file with given ID.
 *
 * @param[in] dmId Message identifier.
 * @return File name.
 */
static
QString delInfoFileName(qint64 dmId)
{
	return QString("DD_%1.zfo").arg(dmId);
}

/*!
 * @brief Create the file name for an attachment file with given ID.
 *
 * @param[in] dmId Message identifier.
 * @param[in] attNum Attachment sequential number as stored in the data message.
 * @param[in] dmFileDescr Attachment file description name as stored in the data message.
 * @return File name.
 */
static
QString attachFileName(qint64 dmId, int attNum, const QString &dmFileDescr)
{
	return QString("ATT_%1_%2_%3").arg(dmId).arg(attNum).arg(dmFileDescr);
}

/*!
 * @brief Get binary data from file.
 *
 * @param[in] assocFileDirPath General path for this database.
 * @param[in] dmId Message identifier.
 * @param[in] fileName File name.
 * @return Empty byte array on error.
 */
static
QByteArray contentFileRaw(const QString &assocFileDirPath, qint64 dmId, const QString &fileName)
{
	const QString dirPath = constructDirPath(assocFileDirPath, dmId);
	if (Q_UNLIKELY(dirPath.isEmpty())) {
		goto fail;
	}

	{
		const QString filePath = dirPath + QStringLiteral("/") + fileName;
		logInfoNL("Reading file '%s'.", filePath.toUtf8().constData());
		{
			const QFileInfo fileInfo(filePath);
			if (Q_UNLIKELY((!fileInfo.isFile()) || (!fileInfo.isReadable()))) {
				goto fail;
			}

			QFile file(filePath);
			if (Q_UNLIKELY(!file.open(QIODevice::ReadOnly))) {
				goto fail;
			}

			QByteArray data = file.readAll();
			file.close();

			return data;
		}
	}

fail:
	return QByteArray();
}

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

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());
	QList<Isds::Document> documents;
	QString queryStr = "SELECT attNum, _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;
			bool ok = false;
			int attNum = query.value(0).toInt(&ok);
			if (Q_UNLIKELY(!ok)) {
				attNum = -1;
			}
			document.setFileDescr(query.value(1).toString());
			document.setUpFileGuid(query.value(2).toString());
			document.setFileGuid(query.value(3).toString());
			document.setMimeType(query.value(4).toString());
			document.setFormat(query.value(5).toString());
			document.setFileMetaType(dbStr2FileMetaType(query.value(6).toString()));
			document.setBase64Content(query.value(7).toString());

			if ((attNum >= 0) && document.binaryContent().isEmpty()) {
				document.setBinaryContent(contentFileRaw(
				    assocFileDirPath(), dmId,
				    attachFileName(dmId, attNum,
				    document.fileDescr())));
			}

			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()) {
		bool ok = false;
		int attNum = query.value(1).toInt(&ok);
		if (Q_UNLIKELY(!ok)) {
			attNum = -1;
		}
		const QString fileDescr = query.value(2).toString();
		QByteArray content;
		if (withContent) {
			content = QByteArray::fromBase64(query.value(4).toByteArray());
			if ((attNum >= 0) && content.isEmpty()) {
				content = contentFileRaw(assocFileDirPath(),
				    dmId, attachFileName(dmId, attNum, fileDescr));
			}
		}
		entryList.append(AttachmentEntry(query.value(0).toLongLong(),
		    dmId, content,
		    fileDescr, 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 message_id, attNum, _dmFileDescr, 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()) {
			const qint64 dmId = query.value(0).toLongLong();
			bool ok = false;
			int attNum = query.value(1).toInt(&ok);
			if (Q_UNLIKELY(!ok)) {
				attNum = -1;
			}
			const QString fileDescr = query.value(2).toString();
			QByteArray content = QByteArray::fromBase64(query.value(3).toByteArray());
			if ((attNum >= 0) && content.isEmpty()) {
				return contentFileRaw(assocFileDirPath(), dmId,
				    attachFileName(dmId, attNum, fileDescr));
			} else {
				return content;
			}
		}
	} 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, dmVODZ, attsNum, read_locally, download_date, "
	    "custom_data) VALUES (:dmId, :message_type, :dmVODZ, :attsNum, "
	    ":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(":dmVODZ", Isds::nilBool2Variant(envelope.dmVODZ()));
	query.bindValue(":attsNum", Isds::nonNegativeLong2Variant(envelope.attsNum()));
	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());
		return false;
	}
	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());
		return false;
	}

	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());
		return false;
	}
	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 (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	/* dmVODZ flag can be NULL or undefined. */
	QString dmVODZ;
	if (envelope.dmVODZ() != Isds::Type::BOOL_NULL) {
		dmVODZ = "dmVODZ = :dmVODZ, ";
	}
	/* attsNum attribute can be NULL or undefined. */
	QString attsNum;
	if (envelope.attsNum() >= 0) {
		attsNum = "attsNum = :attsNum, ";
	}

	queryStr = "UPDATE supplementary_message_data SET "
	    + dmVODZ + attsNum +
	    "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());
		return false;
	}
	if (MSG_RECEIVED == msgDirect) {
		query.bindValue(":message_type", MessageDb::TYPE_RECEIVED);
	} else {
		query.bindValue(":message_type", MessageDb::TYPE_SENT);
	}
	if (!dmVODZ.isEmpty()) {
		query.bindValue(":dmVODZ", Isds::nilBool2Variant(envelope.dmVODZ()));
	}
	if (!attsNum.isEmpty()) {
		query.bindValue(":attsNum", Isds::nonNegativeLong2Variant(envelope.attsNum()));
	}
	query.bindValue(":dmId", envelope.dmId());
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}

	return true;
}

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, "
	    "s.dmVODZ, "
	    "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(9 == query.record().count());

				SoughtMsg foundMsgData(
				    query.value(0).toLongLong(),
				    dateTimeFromDbFormat(query.value(1).toString()),
				    query.value(2).toBool(),
				    int2messageType(query.value(6).toInt()),
				    query.value(3).toString(),
				    query.value(4).toString(),
				    query.value(5).toString(),
				    dateTimeFromDbFormat(query.value(7).toString()),
				    query.value(8).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, "
	    "s.dmVODZ, "
	    "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()),
			    query.value(2).toBool(),
			    int2messageType(query.value(6).toInt()),
			    query.value(3).toString(),
			    query.value(4).toString(),
			    query.value(5).toString(),
			    dateTimeFromDbFormat(query.value(7).toString()),
			    query.value(8).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 (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	{
		const QString dirPath = constructDirPath(assocFileDirPath(), dmId);

		/* Remove associated attachment files. */
		QDirIterator it(dirPath, {"ATT_*.*"});
		while (it.hasNext()) {
			QFile(it.next()).remove();
		}

		/* Remove directory if left empty. */
		QDir dir(dirPath);
		/* dir.isEmpty() missing in the old Qt versions. */
		if (dir.count() == 0) {
			const QString dirName = dir.dirName();
			if (dir.cdUp() && (!dirName.isEmpty())) {
				dir.rmdir(dirName);
			}
		}
	}

	return true;

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.
 *
 * @note This function doesn't store the signing certificate.
 *
 * @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.
 * @param[in] assocFileDirPath If non-empty, then is specifies the directory
 *                             where to store the message ZFO file.
 * @return True on success.
 */
static
bool insertOrReplaceMessageRawDb(QSqlQuery &query, qint64 dmId,
    const QByteArray &raw, enum MessageDb::MessageType messageType)
{
	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;
	}

	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.
 * @param[in] assocFileDirPath If non-empty, then is specifies the directory
 *                             where to store the message ZFO file.
 * @return True on success.
 */
static
bool insertOrReplaceCompleteRaw(QSqlQuery &query, qint64 dmId,
    const QByteArray &raw, enum MessageDb::MessageType messageType,
    const QString &assocFileDirPath)
{
	QString filePathPart;
	if (!assocFileDirPath.isEmpty()) {
		/* Store raw message as temporary ZFO file. */
		const QString dirPath = constructDirPath(assocFileDirPath, dmId);
		if (Q_UNLIKELY(!createDirRecursive(dirPath))) {
			return false;
		}

		const QString filePath = dirPath + QStringLiteral("/") +
		    msgZfoFileName(messageType, dmId);
		filePathPart = filePath + QStringLiteral(".part");
		if (Q_UNLIKELY(WF_SUCCESS != writeFile(filePathPart, raw, true))) {
			return false;
		}

		if (Q_UNLIKELY(!insertOrReplaceMessageRawDb(query, dmId,
		        QByteArray(), messageType))) {
			goto fail;
		}

		/* Move temporary into final location. */
		QFile::remove(filePath);
		QFile::rename(filePathPart, filePath);

	} else {

		if (Q_UNLIKELY(!insertOrReplaceMessageRawDb(query, dmId,
		        raw, messageType))) {
			goto fail;
		}
	}

	/* Get certificate data. */
	{
		struct x509_crt *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:
	if (!filePathPart.isEmpty()) {
		QFile::remove(filePathPart);
	}
	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.
 * @param[in] attNum Attachment number (starting from 0).
 * @param[in] storeRawInDb Whether to store raw data in database file.
 * @return True on success.
 */
static
bool insertOrUpdateAttachmentDb(QSqlQuery &query, qint64 dmId,
    const Isds::Document &document, int attNum, bool storeRawInDb)
{
	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 "
		    "attNum = :attNum, "
		    " _dmFileDescr = :_dmFileDescr, "
		    "_dmUpFileGuid = :_dmUpFileGuid,"
		    " _dmFileGuid = :_dmFileGuid, _dmMimeType = :_dmMimeType, "
		    "_dmFormat = :_dmFormat, "
		    "_dmFileMetaType = :_dmFileMetaType, "
		    "dmEncodedContent = :dmEncodedContent "
		    "WHERE id = :fileId";
	} else {
		queryStr = "INSERT INTO files "
		    "(message_id, attNum, _dmFileDescr, _dmUpFileGuid, _dmFileGuid, "
		    "_dmMimeType, _dmFormat, _dmFileMetaType, dmEncodedContent"
		    ") VALUES ("
		    ":message_id, :attNum, :_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(":attNum", (attNum >= 0) ? attNum : QVariant());
	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", storeRawInDb ? nullVariantWhenIsNull(document.base64Content()) : QVariant());
	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 file into files table.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @param[in] document Document.
 * @param[in] attNum Attachment number (starting from 0).
 * @param[in] dirPath Directory where to store the attachment file.
 * @return True on success.
 */
static
bool insertOrUpdateAttachment(QSqlQuery &query, qint64 dmId,
    const Isds::Document &document, int attNum, const QString &dirPath)
{
	QString filePathPart;
	if (!dirPath.isEmpty()) {
		/* Store raw attachment as temporary file. */
		const QString filePath = dirPath + QStringLiteral("/") +
		    attachFileName(dmId, attNum, document.fileDescr());
		filePathPart = filePath + QStringLiteral(".part");
		if (Q_UNLIKELY(WF_SUCCESS !=
		        writeFile(filePathPart, document.binaryContent(), true))) {
			return false;
		}

		if (Q_UNLIKELY(!insertOrUpdateAttachmentDb(query, dmId,
		        document, attNum, false))) {
			goto fail;
		}

		/* Move temporary into final location. */
		QFile::remove(filePath);
		QFile::rename(filePathPart, filePath);

	} else {

		if (Q_UNLIKELY(!insertOrUpdateAttachmentDb(query, dmId,
		        document, attNum, true))) {
			goto fail;
		}
	}

	return true;

fail:
	if (!filePathPart.isEmpty()) {
		QFile::remove(filePathPart);
	}
	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, const QString &assocFileDirPath)
{
	QString dirPath;
	if (!assocFileDirPath.isEmpty()) {
		dirPath = constructDirPath(assocFileDirPath, dmId);
		if (Q_UNLIKELY(!createDirRecursive(dirPath))) {
			return false;
		}
	}

	for (int i = 0; i < documents.size(); ++i) {
		const Isds::Document &document = documents.at(i);
		if (insertOrUpdateAttachment(query, dmId, document, i, dirPath)) {
			logDebugLv0NL(
			    "Attachment file '%s' of message '%" PRId64 "' has been updated.",
			    document.fileDescr().toUtf8().constData(),
			    UGLY_QINT64_CAST dmId);
		} else {
			logErrorNL(
			    "Updating attachment file '%s' of message '%" PRId64 "' failed.",
			    document.fileDescr().toUtf8().constData(),
			    UGLY_QINT64_CAST dmId);
			goto fail;
		}
	}

	return true;

fail:
	return false;
}

bool MessageDb::insertOrReplaceCompleteMessage(bool storeRawInDb,
    const Isds::Message &msg,
    enum MessageDirection msgDirect, bool verified, bool transaction)
{
	qint64 dmId = msg.envelope().dmId();

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		if (SQLiteDb::memoryLocation == fileName()) {
			/* Always store in database if held in memory. */
			storeRawInDb = true;
		}

		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(),
		        (MSG_RECEIVED == msgDirect) ? TYPE_RECEIVED : TYPE_SENT,
		        storeRawInDb ? QString() : assocFileDirPath())) {
			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(),
		        storeRawInDb ? QString() : assocFileDirPath()))) {
			goto fail;
		}

		if (transaction) {
			commitTransaction();
		}
		goto success;

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

success:
	/* Signal must not be emitted when write lock is active. */
	emit messageInserted(MsgId(dmId, msg.envelope().dmDeliveryTime()),
	    msgDirect);
	return true;
}

bool MessageDb::insertOrUpdateMessageAttachments(bool storeRawInDb, 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,
	    storeRawInDb ? QString() : assocFileDirPath());
	if (Q_UNLIKELY(!ret)) {
		goto fail;
	}

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

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

/*!
 * @brief Determine type of stored raw data.
 */
enum DataEntryType {
	DATA_NONE = 0, /*!< Don't have data. */
	DATA_DB_BASE64 = 1, /*<! Have Base64 data in database. */
	DATA_FILE_RAW = 2 /*!< Have raw data in file. */
};

/*!
 * @brief Check whether the database has the data message stored as a Base64 or a file.
 *
 * @param[in]  query SQL query to work with.
 * @param[in]  dmId Message identifier.
 * @param[out] fileName File name (not a path) of the supposed file.
 * @return Type of stored raw data.
 */
static
enum DataEntryType haveMessageZfo(QSqlQuery &query, qint64 dmId,
    QString *fileName = Q_NULLPTR)
{
	enum DataEntryType det = DATA_NONE;

	QString queryStr =
	    "SELECT message_type, (ifnull(data, 0) != 0) AS have_base64 "
	    "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()) {
		if (query.first() && query.isValid()) {
			const int messageType = query.value(0).toInt(0);
			const bool haveBase64 = query.value(1).toBool();
			if (haveBase64) {
				det = DATA_DB_BASE64;
			} else if ((MessageDb::TYPE_RECEIVED == messageType) ||
			    (MessageDb::TYPE_SENT == messageType)) {
				det = DATA_FILE_RAW;
				if (Q_NULLPTR != fileName) {
					*fileName = msgZfoFileName(
					    (MessageDb::TYPE_RECEIVED == messageType) ?
					        MessageDb::TYPE_RECEIVED : MessageDb::TYPE_SENT,
					    dmId);
				}

			} else {
				logErrorNL("Unknown message_type '%d' in raw_message_data.",
				    messageType);
			}
		} else {
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return det;
}

/*!
 * @brief Get Base64 encoded raw message data from database.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @return Empty byte array on error.
 */
static
QByteArray completeMessageDbBase64(QSqlQuery &query, qint64 dmId)
{
	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();
}

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

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString fileName;
	enum DataEntryType det = haveMessageZfo(query, dmId, &fileName);

	switch (det) {
	case DATA_NONE:
		break;
	case DATA_DB_BASE64:
		return completeMessageDbBase64(query, dmId);
		break;
	case DATA_FILE_RAW:
		return contentFileRaw(assocFileDirPath(), dmId, fileName).toBase64();
		break;
	default:
		Q_ASSERT(0);
		break;
	}

	return QByteArray();
}

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

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	return DATA_NONE != haveMessageZfo(query, dmId);
}

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

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString fileName;
	enum DataEntryType det = haveMessageZfo(query, dmId, &fileName);

	switch (det) {
	case DATA_NONE:
		break;
	case DATA_DB_BASE64:
		return QByteArray::fromBase64(completeMessageDbBase64(query, dmId));
		break;
	case DATA_FILE_RAW:
		return contentFileRaw(assocFileDirPath(), dmId, fileName);
		break;
	default:
		Q_ASSERT(0);
		break;
	}

	return QByteArray();
}

/*!
 * @brief Check whether the database has the delivery info stored as a Base64 or a file.
 *
 * @param[in]  query SQL query to work with.
 * @param[in]  dmId Message identifier.
 * @param[out] fileName File name (not a path) of the supposed file.
 * @return Type of stored raw data.
 */
static
enum DataEntryType haveDeliveryInfoZfo(QSqlQuery &query, qint64 dmId,
    QString *fileName = Q_NULLPTR)
{
	enum DataEntryType det = DATA_NONE;

	QString queryStr =
	    "SELECT (ifnull(data, 0) != 0) as have_base64 "
	    "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()) {
		if (query.first() && query.isValid()) {
			const bool haveBase64 = query.value(0).toBool();
			if (haveBase64) {
				det = DATA_DB_BASE64;
			} else {
				det = DATA_FILE_RAW;
				if (Q_NULLPTR != fileName) {
					*fileName = delInfoFileName(dmId);
				}
			}
		} else {
			goto fail;
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

fail:
	return det;
}

/*!
 * @brief Get Base64 encoded raw delivery info data from database.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @return Empty byte array on error.
 */
static
QByteArray deliveryInfoDbBase64(QSqlQuery &query, qint64 dmId)
{
	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();
}

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

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString fileName;
	enum DataEntryType det = haveDeliveryInfoZfo(query, dmId, &fileName);

	switch (det) {
	case DATA_NONE:
		break;
	case DATA_DB_BASE64:
		return deliveryInfoDbBase64(query, dmId);
		break;
	case DATA_FILE_RAW:
		return contentFileRaw(assocFileDirPath(), dmId, fileName).toBase64();
		break;
	default:
		Q_ASSERT(0);
		break;
	}

	return QByteArray();
}

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

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString fileName;
	enum DataEntryType det = haveDeliveryInfoZfo(query, dmId, &fileName);

	switch (det) {
	case DATA_NONE:
		break;
	case DATA_DB_BASE64:
		return QByteArray::fromBase64(deliveryInfoDbBase64(query, dmId));
		break;
	case DATA_FILE_RAW:
		return contentFileRaw(assocFileDirPath(), dmId, fileName);
		break;
	default:
		Q_ASSERT(0);
		break;
	}

	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)
{
	/* Assure consistency of attached file. */
	{
		MessageDb db("ATTACHED_DATABASE");

		if (Q_UNLIKELY(!db.openDb(newDbFileName, CREATE_MISSING))) {
			return false;
		}

		db.accessDb();

		if (Q_UNLIKELY(!db.isOpen())) {
			return false;
		}
	}

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

/*!
 * @brief Insert/update raw (DER) delibery info data into raw_delivery_info_data table.
 *
 * @param[in] query SQL query to work with.
 * @param[in] dmId Message identifier.
 * @param[in] raw Raw (non-base64 encoded) delivery info data.
 * @return True on success.
 */
static
bool insertOrReplaceDeliveryInfoRawDb(QSqlQuery &query, qint64 dmId,
    const QByteArray &raw)
{
	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::insertOrReplaceDeliveryInfoRaw(bool storeRawInDb, qint64 dmId,
    const QByteArray &raw)
{
	QMutexLocker locker(&m_lock);

	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	if (SQLiteDb::memoryLocation == fileName()) {
		/* Always store in database if held in memory. */
		storeRawInDb = true;
	}

	QString filePathPart;
	if (!storeRawInDb) {
		/* Store raw delivery info as temporary ZFO file. */
		const QString dirPath = constructDirPath(assocFileDirPath(), dmId);
		if (Q_UNLIKELY(!createDirRecursive(dirPath))) {
			return false;
		}

		const QString filePath = dirPath + QStringLiteral("/") +
		    delInfoFileName(dmId);
		filePathPart = filePath + QStringLiteral(".part");
		if (Q_UNLIKELY(WF_SUCCESS != writeFile(filePathPart, raw, true))) {
			return false;
		}

		if (Q_UNLIKELY(!insertOrReplaceDeliveryInfoRawDb(query, dmId,
		        QByteArray()))) {
			goto fail;
		}

		/* Move temporary into final location. */
		QFile::remove(filePath);
		QFile::rename(filePathPart, filePath);

	} else {

		if (Q_UNLIKELY(!insertOrReplaceDeliveryInfoRawDb(query, dmId, raw))) {
			goto fail;
		}
	}
	return true;
fail:
	if (!filePathPart.isEmpty()) {
		QFile::remove(filePathPart);
	}
	return false;
}

bool MessageDb::updateMessageAuthorInfo2(qint64 dmId,
    const Isds::DmMessageAuthor &msgAuthor)
{
	QMutexLocker locker(&m_lock);
	QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

	QString json;
	{
		QJsonDocument document;
		{
			QJsonObject object;
			{
				QJsonValue jsonVal;
				if (Q_UNLIKELY(!Isds::Json::toJsonVal(msgAuthor,
				        jsonVal))) {
					return false;
				}
				object.insert("message_author2", jsonVal);
			}
			document.setObject(object);
		}
		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());
		return false;
	}
	query.bindValue(":dmId", dmId);
	query.bindValue(":custom_data", nullVariantWhenIsNull(json));
	if (Q_UNLIKELY(!query.exec())) {
		logErrorNL("Cannot execute SQL query: %s.",
		    query.lastError().text().toUtf8().constData());
		return false;
	}
	return true;
}

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)
{
	bool emitAtEnd = false;
	MsgId msgId(dmId, QDateTime());

	{
		QMutexLocker locker(&m_lock);

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

		int certificateId = -1;
		bool deleteCert = false;

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

		/* Delete associated files. */
		{
			const QString dirPath = constructDirPath(assocFileDirPath(), dmId);
			if (!dirPath.isEmpty()) {
				QDir(dirPath).removeRecursively();
			}
		}
		goto success;

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

success:
	if (emitAtEnd) {
		/* Signal must not be emitted when write lock is active. */
		emit messageDataDeleted(msgId);
	}
	return true;
}

QList<MsgId> MessageDb::msgsDateInterval(const QDate &fromDate,
    const QDate &toDate, enum MessageType messageType)
{
	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";
	if (fromDate.isValid()) {
		queryStr +=
		    " AND (strftime('%Y-%m-%d', m.dmDeliveryTime) >= :fromDate)";
	}
	if (toDate.isValid()) {
		queryStr +=
		    " 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;
	}
	query.bindValue(":message_type", messageType);
	if (fromDate.isValid()) {
		query.bindValue(":fromDate", fromDate);
	}
	if (toDate.isValid()) {
		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(Utility::dateTimeDisplayFormat));
		messageItems.append(
		    dateTimeFromDbFormat(query.value(4).toString())
		        .toString(Utility::dateTimeDisplayFormat));
		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)
{
	QList<MsgId> toBeChanged;

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		QString queryStr;
		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());
			return false;
		}
		query.bindValue(":state", (int) state);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	}

	/* Signal must not be emitted when write lock is active. */
	emit changedProcessState(toBeChanged, state);
	return true;
}

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)
{
	QList<MsgId> toBeChanged;

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		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());
			return false;
		}
		query.bindValue(":state", (int) state);
		query.bindValue(":message_type", TYPE_RECEIVED);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	}

	/* Signal must not be emitted when write lock is active. */
	emit changedProcessState(toBeChanged, state);
	return true;
}

/*!
 * @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)
{
	QList<MsgId> toBeChanged;

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		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());
			return false;
		}
		query.bindValue(":state", (int) state);
		query.bindValue(":year", nullVariantWhenIsNull(year));
		query.bindValue(":message_type", TYPE_RECEIVED);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	}

	/* Signal must not be emitted when write lock is active. */
	emit changedProcessState(toBeChanged, state);
	return true;
}

/*!
 * @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)
{
	QList<MsgId> toBeChanged;

	{
		QMutexLocker locker(&m_lock);

		QSqlQuery query(DelayedAccessSQLiteDb::accessDb());

		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());
			return false;
		}
		query.bindValue(":state", (int) state);
		query.bindValue(":message_type", TYPE_RECEIVED);
		if (Q_UNLIKELY(!query.exec())) {
			logErrorNL("Cannot execute SQL query: %s.",
			    query.lastError().text().toUtf8().constData());
			return false;
		}
	}

	/* Signal must not be emitted when write lock is active. */
	emit changedProcessState(toBeChanged, state);
	return true;
}

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.
 *
 * @param[in] mDb Message database.
 * @param[in] db Database reference.
 * @return True on success.
 *
 * TODO -- This method may be removed in some future version
 *     of the programme.
 */
static
bool ensurePrimaryKeyInProcessStateTable(MessageDb &mDb, const QSqlDatabase &db)
{
	QSqlQuery query(db);
	QString queryStr;
	QString createTableSql;
	bool transaction = false;

	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. */

	transaction = mDb.beginTransaction();
	if (Q_UNLIKELY(!transaction)) {
		goto fail;
	}

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

	if (transaction) {
		mDb.commitTransaction();
	}

	return true;

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

/*!
 * @brief This method ensures that the files table contains the attNum column.
 *
 * @param[in] mDb Message database.
 * @param[in] db Database reference.
 * @return True on success.
 *
 * TODO -- This method may be removed in some future version
 *     of the programme.
 */
static
bool ensureColumntAttNumInFilesTable(MessageDb &mDb, const QSqlDatabase &db)
{
	QSqlQuery query(db);
	QString queryStr;
	QString createTableSql;
	bool transaction = false;

	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("files"));
	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("attNum", Qt::CaseSensitive)) {
		return true;
	}

	/* Table does not contain 'attNum' column. */

	transaction = mDb.beginTransaction();
	if (Q_UNLIKELY(!transaction)) {
		goto fail;
	}

	/* Rename existing table. */
	queryStr = "ALTER TABLE files RENAME TO _files";
	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(!flsTbl.createEmpty(db))) {
		goto fail;
	}

	/* Copy table content. */
	queryStr = "INSERT OR REPLACE INTO files (message_id, _dmFileDescr, "
	    "_dmUpFileGuid, _dmFileGuid, _dmMimeType, _dmFormat, "
	    "_dmFileMetaType, dmEncodedContent) "
	    "SELECT message_id, _dmFileDescr, _dmUpFileGuid, _dmFileGuid, "
	    "_dmMimeType, _dmFormat, _dmFileMetaType, dmEncodedContent "
	    "FROM _files";
	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 _files";
	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 (transaction) {
		mDb.commitTransaction();
	}

	return true;

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

/*!
 * @brief This method ensures that the supplementary_message_data table
 *     contains the dmVODZ and attsNum column.
 *
 * @param[in] mDb Message database.
 * @param[in] db Database reference.
 * @return True on success.
 *
 * TODO -- This method may be removed in some future version
 *     of the programme.
 */
static
bool ensureDmVODZAndAttsNumColumnInSupMsgData(MessageDb &mDb,
    const QSqlDatabase &db)
{
	QSqlQuery query(db);
	QString queryStr;
	QString createTableSql;
	bool transaction = false;

	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("supplementary_message_data"));
	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("dmVODZ", Qt::CaseSensitive)) {
		return true;
	}

	/*
	 * Table does not contain 'dmVODZ' or 'attNum' column as they are
	 * added in one pass.
	 */

	transaction = mDb.beginTransaction();
	if (Q_UNLIKELY(!transaction)) {
		goto fail;
	}

	/* Rename existing table. */
	queryStr = "ALTER TABLE supplementary_message_data "
	    "RENAME TO _supplementary_message_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 and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

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

	/* Copy table content. */
	queryStr = "INSERT OR REPLACE INTO supplementary_message_data (message_id, message_type, read_locally, download_date, custom_data) "
	    "SELECT message_id, message_type, read_locally, download_date, custom_data FROM _supplementary_message_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 and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	/* Delete old table. */
	queryStr = "DROP TABLE _supplementary_message_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 and/or read SQL data: %s.",
		    query.lastError().text().toUtf8().constData());
		goto fail;
	}

	if (transaction) {
		mDb.commitTransaction();
	}

	return true;

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

bool MessageDb::assureConsistency(void)
{
	QMutexLocker locker(&m_lock);

	logInfoNL(
	    "Assuring primary key in process_state table in database '%s'.",
	    fileName().toUtf8().constData());
	bool ret = ensurePrimaryKeyInProcessStateTable(*this,
	    DelayedAccessSQLiteDb::accessDb());
	if (Q_UNLIKELY(!ret)) {
		logErrorNL(
		    "Couldn't assure primary key in process_state table in database '%s'.",
		    fileName().toUtf8().constData());
		goto fail;
	}

	logInfoNL("Assuring attNum column in files table in database '%s'.",
	    fileName().toUtf8().constData());
	ret = ensureColumntAttNumInFilesTable(*this,
	    DelayedAccessSQLiteDb::accessDb());
	if (Q_UNLIKELY(!ret)) {
		logErrorNL(
		    "Couldn't assure attNum column in files table in database '%s'.",
		    fileName().toUtf8().constData());
		goto fail;
	}

	logInfoNL("Assuring dmVODZ, attsNum columns in supplementary_message_data table in database '%s'.",
	    fileName().toUtf8().constData());
	ret = ensureDmVODZAndAttsNumColumnInSupMsgData(*this,
	    DelayedAccessSQLiteDb::accessDb());
	if (Q_UNLIKELY(!ret)) {
		logErrorNL(
		    "Couldn't assure dmVODZ, attsNum columns in supplementary_message_data table in database '%s'.",
		    fileName().toUtf8().constData());
		goto fail;
	}

	return ret;
fail:
	return false;
}

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

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)
{
	/* Assure consistency of attached file. */
	{
		MessageDb db("ATTACHED_DATABASE");

		if (Q_UNLIKELY(!db.openDb(sourceDbPath, CREATE_MISSING))) {
			return false;
		}

		db.accessDb();

		if (Q_UNLIKELY(!db.isOpen())) {
			return false;
		}
	}

	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, attNum, _dmFileDescr, _dmUpFileGuid, _dmFileGuid, "
	    "_dmMimeType, _dmFormat, _dmFileMetaType, dmEncodedContent) "
	    "SELECT "
	    "message_id, attNum, _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;
}

QByteArray MessageDb::messageAuthorJsonStr(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))) {
		goto fail;
	}
	query.bindValue(":dmID", dmId);
	if (query.exec() && query.isActive()) {
		query.first();
		if (query.isValid()) {
			return query.value(0).toByteArray();
		}
	} else {
		logErrorNL("Cannot execute SQL query: %s",
		    query.lastError().text().toUtf8().constData());
	}

fail:
	return QByteArray();
}
