/*
 * Copyright (C) 2014-2026 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 <QDesktopServices>
#include <QFileDialog>
#include <QMimeDatabase>
#include <QMimeType>
#include <QProcess>

#if defined(Q_OS_WIN)
#  include <QAxObject>
#  include <QUuid>
#  include <windows.h>
#  include <mapi.h> /* MAPISendMail */
#  if defined(USE_MAPI_W_INCLUDE)
#    include "src/system/win_mapi_w.h" /* MAPISendMailW */
#  endif /* USE_MAPI_W_INCLUDE */

/*
 * Note to QString to PWSTR conversion.
 *
 * QString is internally UTF-16.
 * According to
 * https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types
 * PWSTR is a pointer to a null-terminated string of 16-bit Unicode characters.
 *
 * Therefore a conversion via std::wstring line in
 * https://stackoverflow.com/questions/68707790/converting-qstring-to-pcwstr-on-windows
 * may be unnecessary.
 *
 * Using approach from:
 * https://forum.qt.io/topic/92621/convert-qstring-to-wchar_t/7
 *
   QString qString("Test");
   wchar_t *wc = reinterpret_cast<wchar_t *>(qString.data())
   const wchar_t *cwc = reinterpret_cast<const wchar_t *>(qString.utf16());
 *
 */

#endif /* Q_OS_WIN */

#include "src/datovka_shared/io/filesystem.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/utility/strings.h" /* generateRandomString */
#include "src/datovka_shared/settings/prefs.h"
#include "src/datovka_shared/utility/date_time.h"
#include "src/global.h"
#include "src/gui/message_operations.h"
#include "src/io/account_db.h"
#include "src/io/filesystem.h"
#include "src/io/message_db.h"
#include "src/io/message_db_set.h"
#include "src/settings/accounts.h"
#include "src/settings/prefs_specific.h"

/*!
 * @brief Appends email footer into \a message.
 *
 * @param[out] message Message body.
 */
static
void appedMessageFooter(QString &message)
{
	const QString newLine("\n"); /* "\r\n" ? */

	message += newLine;
	message += "-- " + newLine; /* Must contain the space. */
	message += " " + GuiMsgOps::tr("Created using the Datovka.cz application") + " " VERSION "." + newLine;
	message += " <URL: " DATOVKA_HOMEPAGE_URL ">" + newLine;
}

/*!
 * @brief Sets email header and message body into \a message.
 *
 * @param[out] message Message body.
 * @param[in] subj Subject string.
 * @param[in] boundary Boundary string.
 */
static
void createEmailMessage(QString &message, const QString &subj,
    const QString &boundary)
{
	message.clear();

	const QString newLine("\n"); /* "\r\n" ? */

	/* Rudimentary header. */
	message += "Subject: " + subj + newLine;
	message += "MIME-Version: 1.0" + newLine;
	message += "Content-Type: multipart/mixed;" + newLine +
	    " boundary=\"" + boundary + "\"" + newLine;

	/* Body. */
	message += newLine;
	message += "--" + boundary + newLine;
	message += "Content-Type: text/plain; charset=UTF-8" + newLine;
	message += "Content-Transfer-Encoding: 8bit" + newLine;

	appedMessageFooter(message);
}

/*!
 * @brief Adds attachment into email message.
 *
 * @param[in,out] message Message body.
 * @param[in] attachName Attachment file name.
 * @param[in] base64 Attachment content in Base64.
 * @param[in] boundary Boundary string.
 */
static
void addAttachmentToEmailMessage(QString &message,
    const QString &attachName, const QByteArray &base64,
    const QString &boundary)
{
	const QString newLine("\n"); /* "\r\n" ? */

	QMimeDatabase mimeDb;

	QMimeType mimeType(
	    mimeDb.mimeTypeForData(QByteArray::fromBase64(base64)));

	/*
	 * For details about file name encoding (percent encoding) see:
	 * https://stackoverflow.com/questions/51719244/how-to-encode-the-filename-parameter-value-of-the-content-disposition-header-in
	 */
	const QString attachnamePercentEncoded(attachName.toUtf8().toPercentEncoding());

	message += newLine;
	message += "--" + boundary + newLine;
	message += "Content-Type: " + mimeType.name() + "; charset=UTF-8;" + newLine +
	    " name*=UTF-8''" + attachnamePercentEncoded + newLine;
	message += "Content-Transfer-Encoding: base64" + newLine;
	message += "Content-Disposition: attachment;" + newLine +
	    " filename*=UTF-8''" + attachnamePercentEncoded + newLine;

	for (int i = 0; i < base64.size(); ++i) {
		if ((i % 60) == 0) {
			message += newLine;
		}
		message += base64.at(i);
	}
	message += newLine;
}

/*!
 * @brief Appends last line into email message.
 *
 * @param[in,out] message Message body.
 * @param[in] boundary Boundary string.
 */
static
void finishEmailMessage(QString &message, const QString &boundary)
{
	const QString newLine("\n"); /* "\r\n" ? */
	message += newLine + "--" + boundary + "--" + newLine;
}

void GuiMsgOps::exportSelectedData(const QList<MsgOrigin> &originList,
    enum Exports::ExportFileType expFileType, QWidget *parent)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	/* Just take the path from the first selected message. */
	QString exportPath = PrefsSpecific::acntZfoDir(
	    *GlobInstcs::prefsPtr, originList.at(0).acntIdDb);
	QString lastPath = exportPath;
	QString errStr;

	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}
		const QString dbId = GlobInstcs::accntDbPtr->dbId(
		    AccountDb::keyFromLogin(origin.acntIdDb.username()));
		const QString accountName = GlobInstcs::acntMapPtr->acntData(
		    origin.acntIdDb).accountName();

		Exports::exportAsGUI(parent, *origin.acntIdDb.messageDbSet(),
		    expFileType, exportPath, QString(),
		    origin.acntIdDb.username(), accountName, dbId, origin.msgId,
		    true, lastPath, errStr);
		if (!lastPath.isEmpty()) {
			exportPath = lastPath;
			PrefsSpecific::setAcntZfoDir(*GlobInstcs::prefsPtr,
			    origin.acntIdDb, exportPath);
		}
	}
}

void GuiMsgOps::exportSelectedEnvelopeAndAttachments(
    const QList<MsgOrigin> &originList, QWidget *parent)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	/* Just take the path from the first selected message. */
	QString newDir = QFileDialog::getExistingDirectory(parent,
	    tr("Select Directory"),
	    PrefsSpecific::acntZfoDir(*GlobInstcs::prefsPtr, originList.at(0).acntIdDb),
	    QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
	if (newDir.isEmpty()) {
		return;
	}

	QString errStr;
	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}
		const QString dbId = GlobInstcs::accntDbPtr->dbId(
		    AccountDb::keyFromLogin(origin.acntIdDb.username()));
		const QString accountName = GlobInstcs::acntMapPtr->acntData(
		    origin.acntIdDb).accountName();

		Exports::exportEnvAndAttachments(
		    *origin.acntIdDb.messageDbSet(), newDir,
		    origin.acntIdDb.username(), accountName, dbId,
		    origin.msgId, errStr);

		PrefsSpecific::setAcntZfoDir(*GlobInstcs::prefsPtr,
		    origin.acntIdDb, newDir);
	}
}

/*!
 * @brief Construct a random boundary line.
 *
 * @return Boundary line string.
 */
static
QString emailBoundary(void)
{
	return "-----" + Utility::generateRandomString(16) + "_" +
	    QDateTime::currentDateTimeUtc().toString(Utility::dataTimeMSecsHyphenFormat);
}

/*!
 * @brief Build e-mail subject string according to number of messages.
 *
 * @param[in] originList List of messages to be worked with.
 * @return Subject string.
 */
static
QString msgCreateSubject(const QList<MsgOrigin> &originList)
{
	QString subject(((1 == originList.size()) ?
	    GuiMsgOps::tr("Data message") : GuiMsgOps::tr("Data messages")) + QLatin1String(": "));

	bool first = true;
	for (const MsgOrigin &origin : originList) {
		if (first) {
			subject += QString::number(origin.msgId.dmId());
			first = false;
		} else {
			subject += QLatin1String(", ") +
			    QString::number(origin.msgId.dmId());
		}
	}

	return subject;
}

/*!
 * @brief Creates temporary *.eml file with selected content and opens the file
 *     in a default application.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 */
static
void msgBuildAndOpenEml(const QList<MsgOrigin> &originList,
    GuiMsgOps::EmailContents attchFlags)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	QString emailMessage;
	const QString boundary = emailBoundary();

	createEmailMessage(emailMessage,
	    msgCreateSubject(originList), boundary);

	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}

		MessageDb *messageDb =
		    origin.acntIdDb.messageDbSet()->accessMessageDb(
		        origin.msgId.deliveryTime(), false);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			continue;
		}

		/* ZFO message */
		if (attchFlags & GuiMsgOps::ADD_ZFO_MESSAGE) {

			const QByteArray base64 =
			    messageDb->getCompleteMessageBase64(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(base64.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("%1_%2.zfo")
			    .arg(Exports::dmTypePrefix(messageDb, origin.msgId.dmId()))
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			addAttachmentToEmailMessage(emailMessage,
			    attachName, base64, boundary);
		}

		/* ZFO delivery info */
		if (attchFlags & GuiMsgOps::ADD_ZFO_DELIVERY_INFO) {

			const QByteArray base64 =
			    messageDb->getDeliveryInfoBase64(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(base64.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("DO_%1.zfo")
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			addAttachmentToEmailMessage(emailMessage,
			    attachName, base64, boundary);
		}

		/* attachments */
		if (attchFlags & GuiMsgOps::ADD_ATTACHMENTS) {

			const QList<Isds::Document> attachList =
			    messageDb->getMessageAttachments(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(attachList.isEmpty())) {
				Q_ASSERT(0);
				return;
			}
			for (const Isds::Document &attach : attachList) {
				if (Q_UNLIKELY(attach.fileDescr().isEmpty() ||
				        attach.binaryContent().isEmpty())) {
					Q_ASSERT(0);
					continue;
				}
				addAttachmentToEmailMessage(emailMessage,
				    attach.fileDescr(),
				    attach.base64Content().toUtf8(),
				    boundary);
			}
		}

	}

	finishEmailMessage(emailMessage, boundary);

	/* Email is encoded using UTF-8. */
	QString tmpEmailFile = writeTemporaryFile(
	    TMP_ATTACHMENT_PREFIX "mail.eml", emailMessage.toUtf8());

	if (!tmpEmailFile.isEmpty()) {
		logInfoNL("Trying opening file '%s'.",
		    tmpEmailFile.toUtf8().constData());
		const QUrl url = QUrl::fromLocalFile(tmpEmailFile);
		if (Q_UNLIKELY(!QDesktopServices::openUrl(url))) {
			logErrorNL("Failed opening file URL '%s'.",
			    url.toString().toUtf8().constData());
		}
	}
}

/*!
 * @brief Specifies which approach to use to create and pass e-mails to e-mail
 *     clients.
 */
enum EmailMethod {
	EMAIL_USE_DEFAULT, /* Default method, whatever that is. */
	EMAIL_USE_EML,
	EMAIL_USE_XDG,
	EMAIL_USE_WIN_AX,
	EMAIL_USE_WIN_MAPI,
	EMAIL_USE_MACOS_OSASCRIPT
};

#if defined(Q_OS_WIN)
/*!
 * @brief Determine which method to use on windows.
 *
 * @return EMAIL_USE_DEFAULT, EMAIL_USE_EML, EMAIL_USE_WIN_AX or EMAIL_USE_WIN_MAPI.
 */
static
enum EmailMethod emailMethodWin(void)
{
	int emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
	{
		qint64 val = 0;
		if (GlobInstcs::prefsPtr->intVal("os_win.action.email.creation_manner", val)) {
			switch (val) {
			case PrefsSpecific::EMAIL_CREATE_DEFAULT:
				emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
				break;
			case PrefsSpecific::EMAIL_CREATE_EML_FILE:
				emailCreation = PrefsSpecific::EMAIL_CREATE_EML_FILE;
				break;
			case PrefsSpecific::EMAIL_CREATE_FORCE_AX:
				emailCreation = PrefsSpecific::EMAIL_CREATE_FORCE_AX;
				break;
			case PrefsSpecific::EMAIL_CREATE_FORCE_MAPI:
				emailCreation = PrefsSpecific::EMAIL_CREATE_FORCE_MAPI;
				break;
			default:
				emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
				break;
			}
		}
	}

	switch (emailCreation) {
	case PrefsSpecific::EMAIL_CREATE_EML_FILE:
		return EMAIL_USE_EML;
		break;
	case PrefsSpecific::EMAIL_CREATE_FORCE_AX:
		return EMAIL_USE_WIN_AX;
		break;
	case PrefsSpecific::EMAIL_CREATE_FORCE_MAPI:
		return EMAIL_USE_WIN_MAPI;
		break;
	default:
		return EMAIL_USE_DEFAULT;
		break;
	}

	return EMAIL_USE_DEFAULT;
}

/*!
 * @brief Adds attachment (DO, ZFO) into email message.
 *
 * @param[in,out] files Attachment files.
 * @param[in,out] paths Paths to files.
 * @param[in,out] displayNames Original attachment names.
 * @param[in] attachName Attachment file name.
 * @param[in] tmpDirPath Path to temporary directory
 * @param[in] data Raw attachment content.
 */
static
void addAttachmentZFOToEmailMessageMAPI(QVector<MapiFileDescW> &files,
    QList<QString> &paths, QList<QString> &displayNames,
    const QString &attachName, const QString &tmpDirPath, const QByteArray &data)
{
	const QString attachAbsPath = tmpDirPath + QDir::separator() + attachName;

	enum WriteFileState state = writeFile(attachAbsPath, data, true);
	if (WF_SUCCESS == state) {
		paths.append(QDir::toNativeSeparators(attachAbsPath));
		displayNames.append(attachName);

		MapiFileDescW desc = { };
		desc.nPosition = (ULONG)-1;
		desc.lpszPathName = reinterpret_cast<wchar_t *>(paths.last().data());
		desc.lpszFileName = reinterpret_cast<wchar_t *>(displayNames.last().data());

		files.append(desc);
	}
}

/*!
 * @brief Adds attachment into email message.
 *
 * @param[in,out] files Attachment files.
 * @param[in,out] paths Paths to files.
 * @param[in,out] displayNames Original attachment names.
 * @param[in] attachName Attachment file name.
 * @param[in] tmpDirPath Path to temporary directory
 * @param[in] data Raw attachment content.
 */
static
void addAttachmentToEmailMessageMAPI(QVector<MapiFileDescW> &files,
    QList<QString> &paths,  QList<QString> &displayNames,
    const QString &attachName, const QString &tmpDirPath, const QByteArray &data)
{
	QFileInfo fi(attachName);
	QString suffix = fi.suffix();
	QString safeASCIIAttachName = QUuid::createUuid().toString(QUuid::WithoutBraces)
	    + "." + fi.suffix();

	const QString attachAbsPath = tmpDirPath + QDir::separator() + safeASCIIAttachName;

	enum WriteFileState state = writeFile(attachAbsPath, data, true);
	if (WF_SUCCESS == state) {
		paths.append(QDir::toNativeSeparators(attachAbsPath));
		displayNames.append(attachName);

		MapiFileDescW desc = { };
		desc.nPosition = (ULONG)-1;
		desc.lpszPathName = reinterpret_cast<wchar_t *>(paths.last().data());
		desc.lpszFileName = reinterpret_cast<wchar_t *>(displayNames.last().data());

		files.append(desc);
	}
}

/*!
 * @brief Creates email using MAPI with selected content and opens it
 *     in a default email application.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 * @return True on success, false on any error.
 */
static
bool createEmailMAPI(const QList<MsgOrigin> &originList,
    GuiMsgOps::EmailContents attchFlags)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return false;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	MapiMessageW message = { };
	QString subj = msgCreateSubject(originList);
	QString messageBody;
	appedMessageFooter(messageBody);

	message.lpszSubject = reinterpret_cast<wchar_t *>(subj.data());
	message.lpszNoteText = reinterpret_cast<wchar_t *>(messageBody.data());

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	/* Using QVector because QList::data() doesn't exist in Qt-5. */
	QVector<MapiFileDescW> files;
	/*
	 * The following lists must remain intact until MAPISendMailW is called
	 * as \a files will contain pointers into the strings held within them.
	 */
	QList<QString> paths;
	QList<QString> displayNames;

	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}

		MessageDb *messageDb =
		    origin.acntIdDb.messageDbSet()->accessMessageDb(
		        origin.msgId.deliveryTime(), false);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			continue;
		}

		/* ZFO message */
		if (attchFlags & GuiMsgOps::ADD_ZFO_MESSAGE) {

			const QByteArray data =
			    messageDb->getCompleteMessageRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("%1_%2.zfo")
			    .arg(Exports::dmTypePrefix(messageDb, origin.msgId.dmId()))
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			addAttachmentZFOToEmailMessageMAPI(files, paths,
			    displayNames, attachName, tmpDirPath, data);
		}

		/* ZFO delivery info */
		if (attchFlags & GuiMsgOps::ADD_ZFO_DELIVERY_INFO) {

			const QByteArray data =
			    messageDb->getDeliveryInfoRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("DO_%1.zfo")
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			addAttachmentZFOToEmailMessageMAPI(files, paths,
			    displayNames, attachName, tmpDirPath, data);
		}

		/* attachments */
		if (attchFlags & GuiMsgOps::ADD_ATTACHMENTS) {

			const QList<Isds::Document> attachList =
			    messageDb->getMessageAttachments(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(attachList.isEmpty())) {
				Q_ASSERT(0);
				return false;
			}
			for (const Isds::Document &attach : attachList) {
				if (Q_UNLIKELY(attach.fileDescr().isEmpty() ||
				    attach.binaryContent().isEmpty())) {
					Q_ASSERT(0);
					continue;
				}

				addAttachmentToEmailMessageMAPI(files, paths,
				    displayNames, attach.fileDescr(),
				    tmpDirPath, attach.binaryContent());
			}
		}

	}

	message.nFileCount = files.size();
	message.lpFiles = files.data();

#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
	/*
	 * Qt-6 uses newer versions of Mingw-w64. It has no problems linking
	 * MAPISendMailW.
	 */

	ULONG result = MAPISendMailW(
	    0,
	    0,
	    &message,
	    MAPI_DIALOG,
	    0);

	return (result == SUCCESS_SUCCESS);
#else /* < Qt-6.0 */
#  warning "Compiling against version < Qt-6 which has problems linking MAPISendMailW()."
	logWarning("%s",
	    "Cannot use MAPISendMailW because of Qt-5 MinGW limitations.");
	return false;
#endif /* >= Qt-6.0 */
}

/*!
 * @brief Adds attachment into email message - COM.
 *
 * @param[in,out] attachs Attachment files.
 * @param[in] attachName Attachment file name.
 * @param[in] tmpDirPath Path to temporary directory
 * @param[in] data Raw attachment content.
 */
static
void addAttachmentToEmailMessageCOM(QAxObject *attachs, const QString &attachName,
    const QString &tmpDirPath, const QByteArray &data)
{
	if (Q_UNLIKELY(Q_NULLPTR == attachs)) {
		Q_ASSERT(0);
		return;
	}

	const QString attachAbsPath = tmpDirPath + QDir::separator() + attachName;

	enum WriteFileState state = writeFile(attachAbsPath, data, true);
	if (WF_SUCCESS == state) {
		attachs->dynamicCall("Add(QString)", QDir::toNativeSeparators(attachAbsPath));
	}
}

/*!
 * @brief Creates email using COM with selected content and opens it
 *     in Outlook classic.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 * @return True on success, false on any error.
 */
static
bool createEmailCOM(const QList<MsgOrigin> &originList,
    GuiMsgOps::EmailContents attchFlags)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return false;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	QAxObject outlook("Outlook.Application");
	if (outlook.isNull()) {
		logInfoNL("%s", "Outlook.Application not available");
		return false;
	}

	QAxObject *mail = Q_NULLPTR;
	QAxObject *attachs = Q_NULLPTR;

	mail = outlook.querySubObject("CreateItem(int)", 0);
	if (Q_UNLIKELY(Q_NULLPTR == mail)) {
		logInfoNL("%s", "Failed to create outlook mail item");
		return false;
	}

	mail->setProperty("Subject", msgCreateSubject(originList));

	QString messageBody;
	appedMessageFooter(messageBody);
	mail->setProperty("Body", messageBody);

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	attachs = mail->querySubObject("Attachments");
	if (Q_UNLIKELY(Q_NULLPTR == attachs)) {
		logErrorNL("%s", "Cannot access attachments");

		delete attachs;
		delete mail;
		return false;
	}


	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}

		MessageDb *messageDb =
		    origin.acntIdDb.messageDbSet()->accessMessageDb(
		        origin.msgId.deliveryTime(), false);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			continue;
		}

		/* ZFO message */
		if (attchFlags & GuiMsgOps::ADD_ZFO_MESSAGE) {

			const QByteArray data =
			    messageDb->getCompleteMessageRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("%1_%2.zfo")
			    .arg(Exports::dmTypePrefix(messageDb, origin.msgId.dmId()))
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			addAttachmentToEmailMessageCOM(attachs, attachName,
			    tmpDirPath, data);
		}

		/* ZFO delivery info */
		if (attchFlags & GuiMsgOps::ADD_ZFO_DELIVERY_INFO) {

			const QByteArray data =
			    messageDb->getDeliveryInfoRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("DO_%1.zfo")
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			addAttachmentToEmailMessageCOM(attachs, attachName,
			    tmpDirPath, data);
		}

		/* attachments */
		if (attchFlags & GuiMsgOps::ADD_ATTACHMENTS) {

			const QList<Isds::Document> attachList =
			    messageDb->getMessageAttachments(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(attachList.isEmpty())) {
				Q_ASSERT(0);

				delete attachs;
				delete mail;
				return false;
			}

			int fileNumber = 0;
			for (const Isds::Document &attach : attachList) {
				if (Q_UNLIKELY(attach.fileDescr().isEmpty() ||
				    attach.binaryContent().isEmpty())) {
					Q_ASSERT(0);
					continue;
				}

				const QString subdirPath = tmpDirPath
				    + QDir::separator()
				    + QString("%1_%2")
				        .arg(origin.msgId.dmId())
				        .arg(fileNumber++);
				{
					/*
					 * Create a separate subdirectory because the files
					 * may have equal names.
					 */
					QDir dir(tmpDirPath);
					if (Q_UNLIKELY(!dir.mkpath(subdirPath))) {
						logError("Could not create directory '%s'.",
						    subdirPath.toUtf8().constData());

						delete attachs;
						delete mail;
						return false;
					}
				}

				addAttachmentToEmailMessageCOM(attachs,
				    attach.fileDescr(), subdirPath,
				    attach.binaryContent());
			}
		}

	}

	mail->dynamicCall("Display()");

	delete attachs;
	delete mail;

	return true;
}
#endif /* Q_OS_WIN */

/*!
 * @brief Call external command in a separate process.
 *     Waits for command to finish.
 *
 * @param[in] command Command name.
 * @param[in] args Command arguments.
 * @return True on success, false on any error.
 */
static
bool callProcess(const QString &command, const QStringList &args)
{
	if (Q_UNLIKELY(command.isEmpty())) {
		return false;
	}

	int haveError = false;
	QProcess::ProcessError processError = QProcess::UnknownError;

	QProcess proc;

#if (QT_VERSION < QT_VERSION_CHECK(5, 6, 0))
	/*
	 * QProcess::error() is an overloaded method in Qt-5.x.
	 * It is a signal and a method.
	 * Let the compiler get the pointer to the signal.
	 *
	 * The construction
	 * QOverload<QProcess::ProcessError>::of(&QProcess::error)
	 * cannot be used as it was introduced in Qt-5.7.
	 */
	void (QProcess::*fp)(QProcess::ProcessError) = &QProcess::error;
#endif /* < Qt-5.6 */


	/* Watch for errors. */
	QObject::connect(&proc,
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
	    &QProcess::errorOccurred,
#else /* < Qt-5.6 */
	    fp,
#endif /* >= Qt-5.6 */
	    [=, &haveError, &processError](QProcess::ProcessError error) { haveError = true; processError = error; });

	proc.setProgram(command);
	proc.setArguments(args);
	proc.start();

	proc.waitForFinished(-1);

	const bool runOk = (QProcess::NormalExit == proc.exitStatus()) && (!haveError);
	if (Q_UNLIKELY(!runOk)) {
		logErrorNL("Cannot execute command '%s'.",
		    (command + " " + args.join(" ")).toUtf8().constData());
	}
	{
		const QByteArray errOut = proc.readAllStandardError();
		if (!errOut.isEmpty()) {
			logErrorNL("Got error output from command '%s': %s",
			    command.toUtf8().constData(),
			    errOut.constData());
		}
	}
	return runOk;
}

#if defined(Q_OS_MACOS)
/*!
 * @brief Determine which method to use on macOS.
 *
 * @return EMAIL_USE_DEFAULT, EMAIL_USE_EML, EMAIL_USE_MACOS_OSASCRIPT.
 */
static
enum EmailMethod emailMethodMacOS(void)
{
	int emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
	{
		qint64 val = 0;
		if (GlobInstcs::prefsPtr->intVal("os_macos.action.email.creation_manner", val)) {
			switch (val) {
			case PrefsSpecific::EMAIL_CREATE_DEFAULT:
				emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
				break;
			case PrefsSpecific::EMAIL_CREATE_EML_FILE:
				emailCreation = PrefsSpecific::EMAIL_CREATE_EML_FILE;
				break;
			case PrefsSpecific::EMAIL_CREATE_FORCE_OSASCRIPT:
				emailCreation = PrefsSpecific::EMAIL_CREATE_FORCE_OSASCRIPT;
				break;
			default:
				emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
				break;
			}
		}
	}

	switch (emailCreation) {
	case PrefsSpecific::EMAIL_CREATE_EML_FILE:
		return EMAIL_USE_EML;
		break;
	case PrefsSpecific::EMAIL_CREATE_FORCE_OSASCRIPT:
		return EMAIL_USE_MACOS_OSASCRIPT;
		break;
	default:
		return EMAIL_USE_DEFAULT;
		break;
	}

	return EMAIL_USE_DEFAULT;
}

/*!
 * @brief Adds attachment into email message - Apple Script.
 *
 * @param[in,out] attachmentList attachment files.
 * @param[in] attachName Attachment file name.
 * @param[in] tmpDirPath Path to temporary directory
 * @param[in] data Raw attachment content.
 */
static
void addAttachmentAppleScript(QStringList &attachmentList, const QString &attachName,
    const QString &tmpDirPath, const QByteArray &data)
{
	const QString attachAbsPath = tmpDirPath + QDir::separator() + attachName;

	enum WriteFileState state = writeFile(attachAbsPath, data, true);
	if (WF_SUCCESS == state) {
	attachmentList << QString("make new attachment with properties"
	    "{file name:POSIX file \"%1\"} "
	    "at after the last paragraph").arg(attachAbsPath);
	}
}

#define OSASCRIPT "/usr/bin/osascript"

/*!
 * @brief Generates Apple Script with selected content and runs the Apple Script
 *     to open it in default email application.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 */
static
void msgBuildAndOpenAppleScript(const QList<MsgOrigin> &originList,
    GuiMsgOps::EmailContents attchFlags)
{
	if (Q_UNLIKELY(originList.isEmpty())) {
		return;
	}

	/*
	 * All entries in the list may not be generated from the same account.
	 * All messages in list are downloaded here.
	 */

	QString subject = msgCreateSubject(originList);
	QString body;
	appedMessageFooter(body);

	QStringList attachmentList;

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}

		MessageDb *messageDb =
		    origin.acntIdDb.messageDbSet()->accessMessageDb(
		        origin.msgId.deliveryTime(), false);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			continue;
		}

		/* ZFO message */
		if (attchFlags & GuiMsgOps::ADD_ZFO_MESSAGE) {

			const QByteArray data =
			    messageDb->getCompleteMessageRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("%1_%2.zfo")
			    .arg(Exports::dmTypePrefix(messageDb, origin.msgId.dmId()))
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			addAttachmentAppleScript(attachmentList, attachName,
			    tmpDirPath, data);
		}

		/* ZFO delivery info */
		if (attchFlags & GuiMsgOps::ADD_ZFO_DELIVERY_INFO) {

			const QByteArray data =
			    messageDb->getDeliveryInfoRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("DO_%1.zfo")
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			addAttachmentAppleScript(attachmentList, attachName,
			    tmpDirPath, data);
		}

		/* attachments */
		if (attchFlags & GuiMsgOps::ADD_ATTACHMENTS) {

			const QList<Isds::Document> attachList =
			    messageDb->getMessageAttachments(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(attachList.isEmpty())) {
				Q_ASSERT(0);
				return;
			}

			int fileNumber = 0;
			for (const Isds::Document &attach : attachList) {
				if (Q_UNLIKELY(attach.fileDescr().isEmpty() ||
				    attach.binaryContent().isEmpty())) {
					Q_ASSERT(0);
					continue;
				}

				const QString subdirPath = tmpDirPath
				    + QDir::separator()
				    + QString("%1_%2")
				        .arg(origin.msgId.dmId())
				        .arg(fileNumber++);
				{
					/*
					 * Create a separate subdirectory because the files
					 * may have equal names.
					 */
					QDir dir(tmpDirPath);
					if (Q_UNLIKELY(!dir.mkpath(subdirPath))) {
						logError("Could not create directory '%s'.",
						    subdirPath.toUtf8().constData());
						return;
					}
				}

				addAttachmentAppleScript(attachmentList,
				    attach.fileDescr(), subdirPath,
				    attach.binaryContent());
			}
		}

	}

	QString script = QString(R"(
	    tell application "Mail"

	    set newMessage to make new outgoing message with properties ¬
	    {subject:"%1", content:"%2", visible:true}

	    tell newMessage
	    %3
	    end tell

	    activate

	    end tell
	    )").arg(subject, body, attachmentList.join("\n"));

	callProcess(OSASCRIPT, {"-e", script});
}
#endif /* Q_OS_MACOS */

#define XDG_EMAIL "xdg-email"

/*!
 * @brief Check the availability of xdg-email.
 *
 * @return EMAIL_USE_EML or EMAIL_USE_XDG.
 */
static
enum EmailMethod useXdgEmail(void)
{
#if !defined(Q_OS_MACOS) && (defined(Q_OS_UNIX) || defined(Q_OS_LINUX))
	int emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
	{
		qint64 val = 0;
		if (GlobInstcs::prefsPtr->intVal("action.email.creation_manner", val)) {
			switch (val) {
			case PrefsSpecific::EMAIL_CREATE_DEFAULT:
				emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
				break;
			case PrefsSpecific::EMAIL_CREATE_EML_FILE:
				emailCreation = PrefsSpecific::EMAIL_CREATE_EML_FILE;
				break;
			case PrefsSpecific::EMAIL_CREATE_FORCE_XDG:
				emailCreation = PrefsSpecific::EMAIL_CREATE_FORCE_XDG;
				break;
			default:
				emailCreation = PrefsSpecific::EMAIL_CREATE_DEFAULT;
				break;
			}
		}
	}

	switch (emailCreation) {
	case PrefsSpecific::EMAIL_CREATE_EML_FILE:
		return EMAIL_USE_EML;
		break;
	case PrefsSpecific::EMAIL_CREATE_FORCE_XDG:
		return EMAIL_USE_XDG;
		break;
	default:
		/* Check the availability of xdg-email. */
		{
			const QString command(XDG_EMAIL);
			bool found = callProcess(command, {"--version"});
			logInfoNL("Command '%s' %s.", command.toUtf8().constData(),
			    found ? "found" : "not found");
			return found ? EMAIL_USE_XDG : EMAIL_USE_EML;
		}
		break;
	}
#endif /* !Q_OS_MACOS && (Q_OS_UNIX || Q_OS_LINUX) */
	return EMAIL_USE_EML;
}

/*!
 * @brief Uses xdg-email to invoke an e-mail client and create an e-mail with
 *     specified content.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 */
static
void msgCallXDGEmail(const QList<MsgOrigin> &originList,
    GuiMsgOps::EmailContents attchFlags)
{
	QString command = XDG_EMAIL;
	QStringList args = {"--subject", msgCreateSubject(originList)};

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	for (const MsgOrigin &origin : originList) {
		if (Q_UNLIKELY(origin.msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}

		MessageDb *messageDb =
		    origin.acntIdDb.messageDbSet()->accessMessageDb(
		        origin.msgId.deliveryTime(), false);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			continue;
		}

		/* ZFO message */
		if (attchFlags & GuiMsgOps::ADD_ZFO_MESSAGE) {

			const QByteArray data =
			    messageDb->getCompleteMessageRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("%1_%2.zfo")
			    .arg(Exports::dmTypePrefix(messageDb, origin.msgId.dmId()))
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			const QString attachAbsPath =
			    tmpDirPath + QDir::separator() + attachName;

			enum WriteFileState state = writeFile(attachAbsPath, data, true);
			if (WF_SUCCESS == state) {
				args += {"--attach", QDir::toNativeSeparators(attachAbsPath)};
			}
		}

		/* ZFO delivery info */
		if (attchFlags & GuiMsgOps::ADD_ZFO_DELIVERY_INFO) {

			const QByteArray data =
			    messageDb->getDeliveryInfoRaw(
			        origin.msgId.dmId());
			if (Q_UNLIKELY(data.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}
			const QString attachName = QString("DO_%1.zfo")
			    .arg(origin.msgId.dmId());
			if (Q_UNLIKELY(attachName.isEmpty())) {
				Q_ASSERT(0);
				continue;
			}

			const QString attachAbsPath =
			    tmpDirPath + QDir::separator() + attachName;

			enum WriteFileState state = writeFile(attachAbsPath, data, true);
			if (WF_SUCCESS == state) {
				args += {"--attach", QDir::toNativeSeparators(attachAbsPath)};
			}
		}

		/* attachments */
		if (attchFlags & GuiMsgOps::ADD_ATTACHMENTS) {

			const QList<Isds::Document> attachList =
			    messageDb->getMessageAttachments(origin.msgId.dmId());
			if (Q_UNLIKELY(attachList.isEmpty())) {
				Q_ASSERT(0);
				return;
			}
			int fileNumber = 0;
			for (const Isds::Document &attach : attachList) {
				if (Q_UNLIKELY(attach.fileDescr().isEmpty() ||
				        attach.binaryContent().isEmpty())) {
					Q_ASSERT(0);
					continue;
				}

				const QString subirPath = tmpDirPath
				    + QDir::separator()
				    + QString("%1_%2")
				        .arg(origin.msgId.dmId())
				        .arg(fileNumber++);
				{
					/*
					 * Create a separate subdirectory because the files
					 * may have equal names.
					 */
					QDir dir(tmpDirPath);
					if (Q_UNLIKELY(!dir.mkpath(subirPath))) {
						logError("Could not create directory '%s'.",
						    subirPath.toUtf8().constData());
						return;
					}
				}

				const QString attachAbsPath =
				    subirPath + QDir::separator() + attach.fileDescr();

				enum WriteFileState state = writeFile(attachAbsPath, attach.binaryContent(), true);
				if (WF_SUCCESS == state) {
					args += {"--attach", QDir::toNativeSeparators(attachAbsPath)};
				}
			}
		}

	}

	{
		QString body;
		appedMessageFooter(body);

		args += {"--body", body};
	}

	callProcess(command, args);
}

void GuiMsgOps::createEmail(const QList<MsgOrigin> &originList,
    EmailContents attchFlags)
{
#if defined(Q_OS_WIN)
	switch (emailMethodWin()) {
	case EMAIL_USE_EML:
		msgBuildAndOpenEml(originList, attchFlags);
		break;
	case EMAIL_USE_WIN_AX:
		createEmailCOM(originList, attchFlags);
		break;
	case EMAIL_USE_WIN_MAPI:
		createEmailMAPI(originList, attchFlags);
		break;
	default:
		if (!createEmailCOM(originList, attchFlags)) {
			if (!createEmailMAPI(originList, attchFlags)) {
				msgBuildAndOpenEml(originList, attchFlags);
			}
		}
		break;
	}
#elif defined(Q_OS_MACOS)
	switch (emailMethodMacOS()) {
	case EMAIL_USE_EML:
		msgBuildAndOpenEml(originList, attchFlags);
		break;
	case EMAIL_USE_MACOS_OSASCRIPT:
		msgBuildAndOpenAppleScript(originList, attchFlags);
		break;
	default:
		msgBuildAndOpenEml(originList, attchFlags);
		break;
	}
#else
	if (EMAIL_USE_XDG == useXdgEmail()) {
		msgCallXDGEmail(originList, attchFlags);
	} else {
		msgBuildAndOpenEml(originList, attchFlags);
	}
#endif
}

/*!
 * @brief Build e-mail subject string, according to number of attachments.
 *
 * @param[in] attachOrigin List of attachments to be worked with.
 * @return Subject string.
 */
static
QString attachCreateSubject(const AttachOrigin &attachOrigin)
{
	return ((1 == attachOrigin.attachData.size())
	    ? GuiMsgOps::tr("Attachment of message %1")
	    : GuiMsgOps::tr("Attachments of message %1"))
	        .arg(attachOrigin.msgId.dmId());
}

#if defined(Q_OS_WIN)
/*!
 * @brief Creates email using MAPI with selected attachments and opens it
 *     in a default email application.
 *
 * @param[in] attachOrigin List of attachments to be worked with.
 * @return True on success, false on any error.
 */
static
bool attachCreateEmailMAPI(const AttachOrigin &attachOrigin)
{
	if (Q_UNLIKELY(attachOrigin.attachData.isEmpty())) {
		return false;
	}

	MapiMessageW message = { };
	QString subj = attachCreateSubject(attachOrigin);
	QString messageBody;
	appedMessageFooter(messageBody);

	message.lpszSubject = reinterpret_cast<wchar_t *>(subj.data());
	message.lpszNoteText = reinterpret_cast<wchar_t *>(messageBody.data());

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	/* Using QVector because QList::data() doesn't exist in Qt-5. */
	QVector<MapiFileDescW> files;
	/*
	 * The following lists must remain intact until MAPISendMailW is called
	 * as \a files will contain pointers into the strings held within them.
	 */
	QList<QString> paths;
	QList<QString> displayNames;

	for (const QPair<QString, QByteArray> &pair : attachOrigin.attachData) {
		const QString &fileName = pair.first;
		const QByteArray &data = pair.second;

		addAttachmentToEmailMessageMAPI(files, paths, displayNames,
		    fileName, tmpDirPath, data);
	}

	message.nFileCount = files.size();
	message.lpFiles = files.data();

#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
	ULONG result = MAPISendMailW(
	    0,
	    0,
	    &message,
	    MAPI_DIALOG,
	    0);

	return (result == SUCCESS_SUCCESS);
#else /* < Qt-6.0 */
#  warning "Compiling against version < Qt-6 which has problems linking MAPISendMailW()."
	logWarning("%s",
	    "Cannot use MAPISendMailW because of Qt-5 MinGW limitations.");
	return false;
#endif /* >= Qt-6.0 */
}

/*!
 * @brief Creates email using COM with selected attachments and opens it
 *     in a Outlook classic.
 *
 * @param[in] attachOrigin List of attachments to be worked with.
 * @return True on success, false on any error.
 */
static
bool attachCreateEmailCOM(const AttachOrigin &attachOrigin)
{
	if (Q_UNLIKELY(attachOrigin.attachData.isEmpty())) {
		return false;
	}

	QAxObject outlook("Outlook.Application");
	if (outlook.isNull()) {
		logInfoNL("%s", "Outlook.Application not available");
		return false;
	}

	QAxObject *mail = Q_NULLPTR;
	QAxObject *attachs = Q_NULLPTR;

	mail = outlook.querySubObject("CreateItem(int)", 0);
	if (Q_UNLIKELY(Q_NULLPTR == mail)) {
		logInfoNL("%s", "Failed to create outlook mail item");
		return false;
	}

	mail->setProperty("Subject", attachCreateSubject(attachOrigin));

	QString messageBody;
	appedMessageFooter(messageBody);
	mail->setProperty("Body", messageBody);

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	attachs = mail->querySubObject("Attachments");
	if (Q_UNLIKELY(Q_NULLPTR == attachs)) {
		logErrorNL("%s", "Cannot access attachments");

		delete attachs;
		delete mail;
		return false;
	}

	int fileNumber = 0;
	for (const QPair<QString, QByteArray> &pair : attachOrigin.attachData) {
		const QString &fileName = pair.first;
		const QByteArray &data = pair.second;

        const QString subdirPath = tmpDirPath
		    + QDir::separator()
		    + QString("%1_%2")
		        .arg(attachOrigin.msgId.dmId())
		        .arg(fileNumber++);
		{
			/*
			 * Create a separate subdirectory because the files
			 * may have equal names.
			 */
			QDir dir(tmpDirPath);
			if (Q_UNLIKELY(!dir.mkpath(subdirPath))) {
				logError("Could not create directory '%s'.",
				    subdirPath.toUtf8().constData());

				delete attachs;
				delete mail;
				return false;
			}
		}

		addAttachmentToEmailMessageCOM(attachs, fileName, tmpDirPath,
		    data);
	}

	mail->dynamicCall("Display()");

	delete attachs;
	delete mail;

	return true;
}
#endif /* Q_OS_WIN */

#if defined(Q_OS_MACOS)
/*!
 * @brief Creates email using Apple Script with selected attachments and opens
 *     it a mail application.
 *
 * @param[in] attachOrigin List of attachments to be worked with.
 */
static
void attachBuildAndOpenAppleScript(const AttachOrigin &attachOrigin)
{
	if (Q_UNLIKELY(attachOrigin.attachData.isEmpty())) {
		return;
	}

	QString subject = attachCreateSubject(attachOrigin);
	QString body;
	appedMessageFooter(body);

	QStringList attachmentList;

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	int fileNumber = 0;
	for (const QPair<QString, QByteArray> &pair : attachOrigin.attachData) {
		const QString &fileName = pair.first;
		const QByteArray &data = pair.second;

		const QString subdirPath = tmpDirPath
		    + QDir::separator()
		    + QString("%1_%2")
		        .arg(attachOrigin.msgId.dmId())
		        .arg(fileNumber++);
		{
			/*
			 * Create a separate subdirectory because the files
			 * may have equal names.
			 */
			QDir dir(tmpDirPath);
			if (Q_UNLIKELY(!dir.mkpath(subdirPath))) {
				logError("Could not create directory '%s'.",
				    subdirPath.toUtf8().constData());
				return;
			}
		}

		addAttachmentAppleScript(attachmentList,fileName, subdirPath, data);
	}

	QString script = QString(R"(
	    tell application "Mail"

	    set newMessage to make new outgoing message with properties ¬
	    {subject:"%1", content:"%2", visible:true}

	    tell newMessage
	    %3
	    end tell

	    activate

	    end tell
	    )").arg(subject, body, attachmentList.join("\n"));

	callProcess(OSASCRIPT, {"-e", script});
}
#endif /* Q_OS_MACOS */

/*!
 * @brief Creates temporary *.eml file with selected content and opens the file
 *     in a default application.
 *
 * @param[in] attachOrigin List of attachments to be worked with.
 */
static
void attachBuildAndOpenEml(const AttachOrigin &attachOrigin)
{
	if (Q_UNLIKELY(attachOrigin.attachData.isEmpty())) {
		return;
	}

	QString emailMessage;
	const QString boundary = emailBoundary();

	createEmailMessage(emailMessage,
	    attachCreateSubject(attachOrigin), boundary);

	for (const QPair<QString, QByteArray> &pair : attachOrigin.attachData) {
		const QString &fileName = pair.first;
		const QByteArray &data = pair.second;

		addAttachmentToEmailMessage(emailMessage,
		    fileName, data.toBase64(), boundary);
	}

	finishEmailMessage(emailMessage, boundary);

	QString tmpEmailFile = writeTemporaryFile(
	    TMP_ATTACHMENT_PREFIX "mail.eml", emailMessage.toUtf8());

	if (!tmpEmailFile.isEmpty()) {
		logInfoNL("Trying opening file '%s'.",
		    tmpEmailFile.toUtf8().constData());
		const QUrl url = QUrl::fromLocalFile(tmpEmailFile);
		if (Q_UNLIKELY(!QDesktopServices::openUrl(url))) {
			logErrorNL("Failed opening file URL '%s'.",
			    url.toString().toUtf8().constData());
		}
	}
}

/*!
 * @brief Uses xdg-email to invoke an e-mail client and create an e-mail with
 *     specified content.
 *
 * @param[in] originList List of messages to be worked with.
 * @param[in] attchFlags Email attachment flags.
 */
static
void attachCallXDGEmail(const AttachOrigin &attachOrigin)
{
	QString command = XDG_EMAIL;
	QStringList args = {"--subject", attachCreateSubject(attachOrigin)};

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);

	int fileNumber = 0;
	for (const QPair<QString, QByteArray> &pair : attachOrigin.attachData) {
		const QString &fileName = pair.first;
		const QByteArray &data = pair.second;

		const QString subirPath = tmpDirPath
		    + QDir::separator()
		    + QString("%1_%2")
		        .arg(attachOrigin.msgId.dmId())
		        .arg(fileNumber++);
		{
			/*
			 * Create a separate subdirectory because the files
			 * may have equal names.
			 */
			QDir dir(tmpDirPath);
			if (Q_UNLIKELY(!dir.mkpath(subirPath))) {
				logError("Could not create directory '%s'.",
				    subirPath.toUtf8().constData());
				return;
			}
		}

		const QString attachAbsPath =
		    subirPath + QDir::separator() + fileName;

		enum WriteFileState state = writeFile(attachAbsPath, data, true);
		if (WF_SUCCESS == state) {
			args += {"--attach", QDir::toNativeSeparators(attachAbsPath)};
		}
	}

	{
		QString body;
		appedMessageFooter(body);

		args += {"--body", body};
	}

	callProcess(command, args);
}

void GuiMsgOps::createEmailAttachments(const AttachOrigin &attachOrigin)
{
#if defined(Q_OS_WIN)
	switch (emailMethodWin()) {
	case EMAIL_USE_EML:
		attachBuildAndOpenEml(attachOrigin);
		break;
	case EMAIL_USE_WIN_AX:
		attachCreateEmailCOM(attachOrigin);
		break;
	case EMAIL_USE_WIN_MAPI:
		attachCreateEmailMAPI(attachOrigin);
		break;
	default:
		if (!attachCreateEmailCOM(attachOrigin)) {
			if (!attachCreateEmailMAPI(attachOrigin)) {
				attachBuildAndOpenEml(attachOrigin);
			}
		}
		break;
	}
#elif defined(Q_OS_MACOS)
	switch (emailMethodMacOS()) {
	case EMAIL_USE_EML:
		attachBuildAndOpenEml(attachOrigin);
		break;
	case EMAIL_USE_MACOS_OSASCRIPT:
		attachBuildAndOpenAppleScript(attachOrigin);
		break;
	default:
		attachBuildAndOpenEml(attachOrigin);
		break;
	}
#else
	if (EMAIL_USE_XDG == useXdgEmail()) {
		attachCallXDGEmail(attachOrigin);
	} else {
		attachBuildAndOpenEml(attachOrigin);
	}
#endif
}
