2026-06-17 22:44:27 +02:00
|
|
|
#include "mailservice.h"
|
2026-07-05 10:45:44 +02:00
|
|
|
#include <QFuture>
|
|
|
|
|
#include <QFutureWatcher>
|
|
|
|
|
#include <QtConcurrent/QtConcurrent>
|
2026-06-17 22:44:27 +02:00
|
|
|
#include <QDebug>
|
2026-08-17 15:34:49 +02:00
|
|
|
#include <QFile>
|
|
|
|
|
#include <QFileInfo>
|
|
|
|
|
#include <QMimeDatabase>
|
|
|
|
|
#include <QRegularExpression>
|
|
|
|
|
#include <QDateTime>
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QJsonDocument>
|
|
|
|
|
#include <QJsonObject>
|
|
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
|
#include <QNetworkReply>
|
|
|
|
|
#include <QNetworkRequest>
|
|
|
|
|
#include <QEventLoop>
|
|
|
|
|
#include <QUrl>
|
|
|
|
|
#include "services/mimestorage.h"
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
QByteArray smtpHeader(const QString &name, const QString &value)
|
|
|
|
|
{
|
|
|
|
|
const QByteArray utf8 = value.toUtf8();
|
|
|
|
|
bool ascii = true;
|
|
|
|
|
for (char c : utf8) {
|
|
|
|
|
if (static_cast<unsigned char>(c) < 32 || static_cast<unsigned char>(c) > 126) {
|
|
|
|
|
ascii = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const QByteArray encoded = ascii
|
|
|
|
|
? utf8
|
|
|
|
|
: QByteArray("=?UTF-8?B?") + utf8.toBase64() + QByteArray("?=");
|
|
|
|
|
return name.toUtf8() + QByteArray(": ") + encoded + QByteArray("\r\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray base64Lines(const QByteArray &data)
|
|
|
|
|
{
|
|
|
|
|
const QByteArray encoded = data.toBase64();
|
|
|
|
|
QByteArray wrapped;
|
|
|
|
|
for (int i = 0; i < encoded.size(); i += 76)
|
|
|
|
|
wrapped.append(encoded.mid(i, 76)).append("\r\n");
|
|
|
|
|
return wrapped;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList smtpRecipients(const QString &value)
|
|
|
|
|
{
|
|
|
|
|
QStringList result;
|
|
|
|
|
for (QString part : value.split(QRegularExpression(QStringLiteral("[,;]")), Qt::SkipEmptyParts)) {
|
|
|
|
|
const QRegularExpressionMatch match = QRegularExpression(QStringLiteral("<([^>]+)>")).match(part);
|
|
|
|
|
if (match.hasMatch()) part = match.captured(1);
|
|
|
|
|
part = part.trimmed();
|
|
|
|
|
if (!part.isEmpty()) result.append(part);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool smtpResponse(QSslSocket &socket, int expectedCode)
|
|
|
|
|
{
|
|
|
|
|
QByteArray line;
|
|
|
|
|
while (true) {
|
|
|
|
|
if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false;
|
|
|
|
|
line = socket.readLine();
|
|
|
|
|
if (line.size() < 3) continue;
|
|
|
|
|
bool ok = false;
|
|
|
|
|
const int code = line.left(3).toInt(&ok);
|
|
|
|
|
if (!ok) continue;
|
|
|
|
|
// A space marks the final line of a multiline SMTP response.
|
|
|
|
|
if (line.size() > 3 && line.at(3) == '-') continue;
|
|
|
|
|
return code == expectedCode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray composeMimeMessage(const MailItem &mail, const Account &account,
|
|
|
|
|
const QStringList &attachmentPaths, QString &error)
|
|
|
|
|
{
|
|
|
|
|
QStringList recipients = smtpRecipients(mail.to());
|
|
|
|
|
if (recipients.isEmpty()) recipients = smtpRecipients(mail.recipient());
|
|
|
|
|
recipients.append(smtpRecipients(mail.cc()));
|
|
|
|
|
recipients.append(smtpRecipients(mail.bcc()));
|
|
|
|
|
if (recipients.isEmpty()) {
|
|
|
|
|
error = QObject::tr("No recipients specified");
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
recipients.removeDuplicates();
|
|
|
|
|
|
|
|
|
|
QByteArray message;
|
|
|
|
|
const QString from = mail.sender().isEmpty() ? account.email() : mail.sender();
|
|
|
|
|
message.append(smtpHeader(QStringLiteral("From"), from));
|
|
|
|
|
message.append(smtpHeader(QStringLiteral("To"), mail.to().isEmpty() ? mail.recipient() : mail.to()));
|
|
|
|
|
if (!mail.cc().isEmpty()) message.append(smtpHeader(QStringLiteral("Cc"), mail.cc()));
|
|
|
|
|
message.append(smtpHeader(QStringLiteral("Subject"), mail.subject()));
|
|
|
|
|
message.append("Date: ").append((mail.date().isValid() ? mail.date() : QDateTime::currentDateTimeUtc()).toString(Qt::RFC2822Date).toUtf8()).append("\r\n");
|
|
|
|
|
message.append("MIME-Version: 1.0\r\n");
|
|
|
|
|
|
|
|
|
|
QByteArray body = mail.bodyHtml().toUtf8();
|
|
|
|
|
body.replace("\r\n", "\n");
|
|
|
|
|
body.replace('\r', '\n');
|
|
|
|
|
body.replace('\n', "\r\n");
|
|
|
|
|
if (attachmentPaths.isEmpty()) {
|
|
|
|
|
message.append("Content-Type: text/html; charset=UTF-8\r\n");
|
|
|
|
|
message.append("Content-Transfer-Encoding: 8bit\r\n\r\n");
|
|
|
|
|
message.append(body).append("\r\n");
|
|
|
|
|
return message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QByteArray boundary = QByteArray("----WinoMail-")
|
|
|
|
|
+ QByteArray::number(QDateTime::currentMSecsSinceEpoch());
|
|
|
|
|
message.append("Content-Type: multipart/mixed; boundary=\"").append(boundary).append("\"\r\n\r\n");
|
|
|
|
|
message.append("--").append(boundary).append("\r\n");
|
|
|
|
|
message.append("Content-Type: text/html; charset=UTF-8\r\n");
|
|
|
|
|
message.append("Content-Transfer-Encoding: 8bit\r\n\r\n").append(body).append("\r\n");
|
|
|
|
|
|
|
|
|
|
QMimeDatabase mimeDatabase;
|
|
|
|
|
for (const QString &path : attachmentPaths) {
|
|
|
|
|
QFile file(path);
|
|
|
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
|
|
|
error = QObject::tr("Could not read attachment: %1").arg(path);
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
const QFileInfo info(path);
|
|
|
|
|
const QString fileName = info.fileName().replace('"', '_');
|
|
|
|
|
const QString mimeType = mimeDatabase.mimeTypeForFile(info).name();
|
|
|
|
|
message.append("--").append(boundary).append("\r\n");
|
|
|
|
|
message.append("Content-Type: ").append(mimeType.toUtf8()).append("; name=\"").append(fileName.toUtf8()).append("\"\r\n");
|
|
|
|
|
message.append("Content-Transfer-Encoding: base64\r\n");
|
|
|
|
|
message.append("Content-Disposition: attachment; filename=\"").append(fileName.toUtf8()).append("\"\r\n\r\n");
|
|
|
|
|
message.append(base64Lines(file.readAll()));
|
|
|
|
|
}
|
|
|
|
|
message.append("--").append(boundary).append("--\r\n");
|
|
|
|
|
return message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool sendSmtp(const MailItem &mail, const Account &account,
|
|
|
|
|
const QStringList &attachmentPaths, QString &error)
|
|
|
|
|
{
|
|
|
|
|
const Account::ConnectionSettings settings = account.connectionSettings();
|
|
|
|
|
if (settings.outgoingHost.isEmpty() || settings.outgoingPort == 0) {
|
|
|
|
|
error = QObject::tr("SMTP server is not configured");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error);
|
|
|
|
|
if (mime.isEmpty()) return false;
|
|
|
|
|
QSslSocket socket;
|
|
|
|
|
if (settings.outgoingSsl && settings.outgoingPort == 465)
|
|
|
|
|
socket.connectToHostEncrypted(settings.outgoingHost, settings.outgoingPort);
|
|
|
|
|
else
|
|
|
|
|
socket.connectToHost(settings.outgoingHost, settings.outgoingPort);
|
|
|
|
|
if (!socket.waitForConnected(15000)) { error = socket.errorString(); return false; }
|
|
|
|
|
if (settings.outgoingSsl && settings.outgoingPort == 465) {
|
|
|
|
|
if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; }
|
|
|
|
|
}
|
|
|
|
|
if (!smtpResponse(socket, 220)) { error = QObject::tr("SMTP greeting failed"); return false; }
|
|
|
|
|
|
|
|
|
|
auto command = [&](const QByteArray &value, int code) {
|
|
|
|
|
socket.write(value + QByteArray("\r\n"));
|
|
|
|
|
socket.flush();
|
|
|
|
|
return smtpResponse(socket, code);
|
|
|
|
|
};
|
|
|
|
|
if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO failed"); return false; }
|
|
|
|
|
if (settings.outgoingSsl && settings.outgoingPort != 465) {
|
|
|
|
|
if (!command("STARTTLS", 220)) { error = QObject::tr("SMTP STARTTLS failed"); return false; }
|
|
|
|
|
socket.startClientEncryption();
|
|
|
|
|
if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; }
|
|
|
|
|
if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO after TLS failed"); return false; }
|
|
|
|
|
}
|
|
|
|
|
const QString username = settings.username.isEmpty() ? account.email() : settings.username;
|
|
|
|
|
if (!username.isEmpty()) {
|
|
|
|
|
if (!command("AUTH LOGIN", 334)
|
|
|
|
|
|| !command(username.toUtf8().toBase64(), 334)
|
|
|
|
|
|| !command(settings.password.toUtf8().toBase64(), 235)) {
|
|
|
|
|
error = QObject::tr("SMTP authentication failed");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const QString from = mail.sender().isEmpty() ? account.email() : mail.sender();
|
|
|
|
|
if (!command("MAIL FROM:<" + smtpRecipients(from).value(0).toUtf8() + ">", 250)) { error = QObject::tr("SMTP MAIL FROM failed"); return false; }
|
|
|
|
|
const QStringList recipients = smtpRecipients(mail.to()) + smtpRecipients(mail.recipient())
|
|
|
|
|
+ smtpRecipients(mail.cc()) + smtpRecipients(mail.bcc());
|
|
|
|
|
QStringList uniqueRecipients = recipients;
|
|
|
|
|
uniqueRecipients.removeDuplicates();
|
|
|
|
|
for (const QString &recipient : uniqueRecipients) {
|
|
|
|
|
if (!command("RCPT TO:<" + recipient.toUtf8() + ">", 250)) { error = QObject::tr("SMTP recipient rejected: %1").arg(recipient); return false; }
|
|
|
|
|
}
|
|
|
|
|
if (!command("DATA", 354)) { error = QObject::tr("SMTP DATA failed"); return false; }
|
|
|
|
|
QByteArray stuffed;
|
|
|
|
|
const QList<QByteArray> lines = mime.split('\n');
|
|
|
|
|
for (QByteArray line : lines) {
|
|
|
|
|
if (line.startsWith('.')) stuffed.append('.');
|
|
|
|
|
stuffed.append(line);
|
|
|
|
|
if (!line.endsWith('\r')) stuffed.append('\r');
|
|
|
|
|
stuffed.append('\n');
|
|
|
|
|
}
|
|
|
|
|
socket.write(stuffed);
|
|
|
|
|
socket.write(".\r\n");
|
|
|
|
|
socket.flush();
|
|
|
|
|
if (!smtpResponse(socket, 250)) { error = QObject::tr("SMTP message rejected"); return false; }
|
|
|
|
|
command("QUIT", 221);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray base64Url(const QByteArray &data)
|
|
|
|
|
{
|
|
|
|
|
QByteArray result = data.toBase64();
|
|
|
|
|
result.replace('+', '-');
|
|
|
|
|
result.replace('/', '_');
|
|
|
|
|
while (result.endsWith('=')) result.chop(1);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool postJson(const QUrl &url, const QByteArray &token, const QJsonObject &payload,
|
|
|
|
|
int expectedStatus, QString &error)
|
|
|
|
|
{
|
|
|
|
|
QNetworkAccessManager network;
|
|
|
|
|
QNetworkRequest request(url);
|
|
|
|
|
request.setRawHeader("Authorization", QByteArrayLiteral("Bearer ") + token);
|
|
|
|
|
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
|
|
|
|
QNetworkReply *reply = network.post(request, QJsonDocument(payload).toJson(QJsonDocument::Compact));
|
|
|
|
|
QEventLoop loop;
|
|
|
|
|
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
|
|
|
|
loop.exec();
|
|
|
|
|
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
|
|
|
const bool ok = reply->error() == QNetworkReply::NoError && status == expectedStatus;
|
|
|
|
|
if (!ok) error = reply->error() == QNetworkReply::NoError
|
|
|
|
|
? QObject::tr("Server returned HTTP %1").arg(status) : reply->errorString();
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
return ok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool sendGmailApi(const MailItem &mail, const Account &account,
|
|
|
|
|
const QStringList &attachmentPaths, QString &error)
|
|
|
|
|
{
|
|
|
|
|
if (!account.isTokenValid()) {
|
|
|
|
|
error = QObject::tr("Gmail access token is missing or expired");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error);
|
|
|
|
|
if (mime.isEmpty()) return false;
|
|
|
|
|
QJsonObject payload;
|
|
|
|
|
payload.insert(QStringLiteral("raw"), QString::fromLatin1(base64Url(mime)));
|
|
|
|
|
const QString user = QString::fromUtf8(QUrl::toPercentEncoding(account.email()));
|
|
|
|
|
const QUrl url(QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/messages/send").arg(user));
|
|
|
|
|
return postJson(url, account.accessToken().toUtf8(), payload, 200, error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonArray graphRecipients(const QString &value)
|
|
|
|
|
{
|
|
|
|
|
QJsonArray result;
|
|
|
|
|
for (const QString &address : smtpRecipients(value)) {
|
|
|
|
|
QJsonObject recipient;
|
|
|
|
|
QJsonObject email;
|
|
|
|
|
email.insert(QStringLiteral("address"), address);
|
|
|
|
|
recipient.insert(QStringLiteral("emailAddress"), email);
|
|
|
|
|
result.append(recipient);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool sendOutlookApi(const MailItem &mail, const Account &account,
|
|
|
|
|
const QStringList &attachmentPaths, QString &error)
|
|
|
|
|
{
|
|
|
|
|
if (!account.isTokenValid()) {
|
|
|
|
|
error = QObject::tr("Outlook access token is missing or expired");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
QJsonObject message;
|
|
|
|
|
message.insert(QStringLiteral("subject"), mail.subject());
|
|
|
|
|
QJsonObject body;
|
|
|
|
|
body.insert(QStringLiteral("contentType"), QStringLiteral("HTML"));
|
|
|
|
|
body.insert(QStringLiteral("content"), mail.bodyHtml());
|
|
|
|
|
message.insert(QStringLiteral("body"), body);
|
|
|
|
|
message.insert(QStringLiteral("toRecipients"), graphRecipients(mail.to().isEmpty() ? mail.recipient() : mail.to()));
|
|
|
|
|
message.insert(QStringLiteral("ccRecipients"), graphRecipients(mail.cc()));
|
|
|
|
|
message.insert(QStringLiteral("bccRecipients"), graphRecipients(mail.bcc()));
|
|
|
|
|
QJsonArray attachments;
|
|
|
|
|
QMimeDatabase mimeDatabase;
|
|
|
|
|
for (const QString &path : attachmentPaths) {
|
|
|
|
|
QFile file(path);
|
|
|
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
|
|
|
error = QObject::tr("Could not read attachment: %1").arg(path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const QFileInfo info(path);
|
|
|
|
|
QJsonObject attachment;
|
|
|
|
|
attachment.insert(QStringLiteral("@odata.type"), QStringLiteral("#microsoft.graph.fileAttachment"));
|
|
|
|
|
attachment.insert(QStringLiteral("name"), info.fileName());
|
|
|
|
|
attachment.insert(QStringLiteral("contentType"), mimeDatabase.mimeTypeForFile(info).name());
|
|
|
|
|
attachment.insert(QStringLiteral("contentBytes"), QString::fromLatin1(file.readAll().toBase64()));
|
|
|
|
|
attachments.append(attachment);
|
|
|
|
|
}
|
|
|
|
|
message.insert(QStringLiteral("attachments"), attachments);
|
|
|
|
|
QJsonObject payload;
|
|
|
|
|
payload.insert(QStringLiteral("message"), message);
|
|
|
|
|
payload.insert(QStringLiteral("saveToSentItems"), true);
|
|
|
|
|
const QUrl url(QStringLiteral("https://graph.microsoft.com/v1.0/me/sendMail"));
|
|
|
|
|
return postJson(url, account.accessToken().toUtf8(), payload, 202, error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2026-06-17 22:44:27 +02:00
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
MailService::MailService(AccountService *accountService, QObject *parent)
|
2026-06-17 22:44:27 +02:00
|
|
|
: QObject(parent)
|
|
|
|
|
, m_composer(new EmailComposerBridge(this))
|
2026-07-05 10:45:44 +02:00
|
|
|
, m_accountService(accountService)
|
2026-06-17 22:44:27 +02:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVector<MailItem> MailService::getMails(const QString &folderId)
|
|
|
|
|
{
|
|
|
|
|
return MailItemDao::findByFolderId(folderId.toInt());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MailService::sendMail(const MailItem &mail, const QString &accountId)
|
|
|
|
|
{
|
2026-08-17 15:34:49 +02:00
|
|
|
sendMail(mail, accountId, {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MailService::sendMail(const MailItem &mail, const QString &accountId,
|
|
|
|
|
const QStringList &attachmentPaths)
|
|
|
|
|
{
|
|
|
|
|
if (!m_accountService) {
|
|
|
|
|
emit mailSendFailed(accountId, tr("AccountService not available"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Account *account = m_accountService->findAccountById(accountId.toLongLong());
|
|
|
|
|
if (!account) {
|
|
|
|
|
emit mailSendFailed(accountId, tr("Account not found"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const Account accountCopy = *account;
|
|
|
|
|
delete account;
|
|
|
|
|
QFuture<bool> future = QtConcurrent::run([mail, accountCopy, attachmentPaths]() {
|
|
|
|
|
QString error;
|
|
|
|
|
bool ok = false;
|
|
|
|
|
if (accountCopy.type() == AccountType::Gmail)
|
|
|
|
|
ok = sendGmailApi(mail, accountCopy, attachmentPaths, error);
|
|
|
|
|
else if (accountCopy.type() == AccountType::Outlook)
|
|
|
|
|
ok = sendOutlookApi(mail, accountCopy, attachmentPaths, error);
|
|
|
|
|
else
|
|
|
|
|
ok = sendSmtp(mail, accountCopy, attachmentPaths, error);
|
|
|
|
|
if (!ok) qWarning() << "Send failed:" << error;
|
|
|
|
|
return ok;
|
|
|
|
|
});
|
|
|
|
|
auto *watcher = new QFutureWatcher<bool>(this);
|
|
|
|
|
connect(watcher, &QFutureWatcher<bool>::finished, this, [this, watcher, accountId]() {
|
|
|
|
|
const bool ok = watcher->result();
|
|
|
|
|
watcher->deleteLater();
|
|
|
|
|
if (ok) emit mailSent(accountId);
|
|
|
|
|
else emit mailSendFailed(accountId, tr("Mail server rejected the message"));
|
|
|
|
|
});
|
|
|
|
|
watcher->setFuture(future);
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
|
|
|
|
{
|
2026-07-05 10:45:44 +02:00
|
|
|
// Determine provider type from account
|
|
|
|
|
if (!m_accountService) {
|
|
|
|
|
emit mailFetchError(accountId, folderId, tr("AccountService not available"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
Account *acc = m_accountService->findAccountById(accountId.toLongLong());
|
2026-08-17 15:34:49 +02:00
|
|
|
if (!acc) {
|
|
|
|
|
emit mailFetchError(accountId, folderId, tr("Account not found"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-07-05 10:45:44 +02:00
|
|
|
QString providerType = QStringLiteral("imap"); // fallback
|
|
|
|
|
if (acc) {
|
|
|
|
|
switch (acc->type()) {
|
|
|
|
|
case AccountType::IMAP:
|
|
|
|
|
providerType = QStringLiteral("imap");
|
|
|
|
|
break;
|
|
|
|
|
case AccountType::POP3:
|
|
|
|
|
providerType = QStringLiteral("pop3");
|
|
|
|
|
break;
|
|
|
|
|
case AccountType::Gmail:
|
|
|
|
|
providerType = QStringLiteral("gmail");
|
|
|
|
|
break;
|
|
|
|
|
case AccountType::Outlook:
|
|
|
|
|
providerType = QStringLiteral("outlook");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
providerType = QStringLiteral("imap");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Synchronizer *sync = SynchronizerProvider::instance().createSynchronizer(accountId, providerType);
|
|
|
|
|
if (!sync) {
|
|
|
|
|
emit mailFetchError(accountId, folderId, tr("Failed to create synchronizer"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 22:53:27 +02:00
|
|
|
|
|
|
|
|
// Initialize synchronizer with account details before using it
|
|
|
|
|
if (!sync->initialize(*acc)) {
|
2026-08-17 15:34:49 +02:00
|
|
|
delete acc;
|
2026-07-08 22:53:27 +02:00
|
|
|
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
|
2026-08-17 15:34:49 +02:00
|
|
|
SynchronizerProvider::instance().unregisterSynchronizer(accountId);
|
2026-07-08 22:53:27 +02:00
|
|
|
return;
|
|
|
|
|
}
|
2026-08-17 15:34:49 +02:00
|
|
|
delete acc;
|
2026-07-08 22:53:27 +02:00
|
|
|
|
2026-07-05 10:45:44 +02:00
|
|
|
// Connect progress signals (if they exist) using string-based syntax for compatibility
|
|
|
|
|
QObject::connect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)), Qt::QueuedConnection);
|
|
|
|
|
QObject::connect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)), Qt::QueuedConnection);
|
|
|
|
|
|
2026-08-23 14:49:58 +02:00
|
|
|
QFuture<QVector<MailItem>> future = QtConcurrent::run([this, sync, folderId]() {
|
2026-07-05 10:45:44 +02:00
|
|
|
// SinceUid = 0 for full sync; could be stored per folder but omitted for simplicity
|
|
|
|
|
return sync->fetchMailItems(folderId, 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
QFutureWatcher<QVector<MailItem>> *watcher = new QFutureWatcher<QVector<MailItem>>(this);
|
2026-08-23 14:49:58 +02:00
|
|
|
QObject::connect(watcher, &QFutureWatcher<QVector<MailItem>>::finished, this, [this, sync, watcher, accountId, folderId]() {
|
2026-07-05 10:45:44 +02:00
|
|
|
// Disconnect progress signals
|
|
|
|
|
QObject::disconnect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)));
|
|
|
|
|
QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)));
|
|
|
|
|
QVector<MailItem> items = watcher->result();
|
2026-08-17 15:34:49 +02:00
|
|
|
QVector<MailItem> persisted;
|
|
|
|
|
for (MailItem &item : items) {
|
|
|
|
|
if (persistFetchedItem(item, accountId, folderId)) persisted.append(item);
|
|
|
|
|
}
|
2026-07-05 10:45:44 +02:00
|
|
|
watcher->deleteLater();
|
2026-08-17 15:34:49 +02:00
|
|
|
emit mailFetched(accountId, folderId, persisted);
|
2026-07-05 10:45:44 +02:00
|
|
|
});
|
|
|
|
|
watcher->setFuture(future);
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-17 15:34:49 +02:00
|
|
|
bool MailService::persistFetchedItem(MailItem &item, const QString &accountId,
|
|
|
|
|
const QString &folderId)
|
|
|
|
|
{
|
|
|
|
|
if (item.rawMime().isEmpty()) return MailItemDao::upsert(item);
|
|
|
|
|
|
|
|
|
|
MimeStorageService storage;
|
|
|
|
|
ParsedMimeMessage parsed;
|
|
|
|
|
QVector<QString> paths;
|
|
|
|
|
if (!storage.parseMessage(item.rawMime(), parsed)
|
|
|
|
|
|| !storage.storeMessage(accountId, folderId, item, item.rawMime(), &paths)) {
|
|
|
|
|
qWarning() << "Failed to persist MIME message" << item.messageId();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!MailItemDao::upsert(item)) {
|
|
|
|
|
storage.deleteEmlFile(item.fileId());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVector<StoredAttachmentRecord> records;
|
|
|
|
|
for (int i = 0; i < parsed.attachments.size(); ++i) {
|
|
|
|
|
const ParsedMimeAttachment &source = parsed.attachments.at(i);
|
|
|
|
|
StoredAttachmentRecord record;
|
|
|
|
|
record.fileName = source.fileName;
|
|
|
|
|
record.mimeType = source.mimeType;
|
|
|
|
|
record.contentId = source.contentId;
|
|
|
|
|
record.size = source.data.size();
|
|
|
|
|
if (i < paths.size()) record.storedPath = paths.at(i);
|
|
|
|
|
records.append(record);
|
|
|
|
|
}
|
|
|
|
|
if (!MailItemDao::replaceAttachments(item.id(), records)) {
|
|
|
|
|
storage.deleteEmlFile(item.fileId());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 22:44:27 +02:00
|
|
|
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
|
|
|
|
|
{
|
2026-07-05 10:45:44 +02:00
|
|
|
bool ok;
|
|
|
|
|
qint64 id = mailItemId.toLongLong(&ok);
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
std::optional<MailItem> opt = MailItemDao::findById(id);
|
|
|
|
|
if (!opt) return;
|
|
|
|
|
MailItem item = *opt;
|
|
|
|
|
item.setFolderId(targetFolderId.toInt());
|
|
|
|
|
if (MailItemDao::update(item))
|
|
|
|
|
emit mailMoved(mailItemId);
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MailService::deleteMail(const QString &mailItemId)
|
|
|
|
|
{
|
2026-07-05 10:45:44 +02:00
|
|
|
bool ok;
|
|
|
|
|
qint64 id = mailItemId.toLongLong(&ok);
|
|
|
|
|
if (!ok) return;
|
2026-08-17 15:34:49 +02:00
|
|
|
const std::optional<MailItem> item = MailItemDao::findById(id);
|
|
|
|
|
if (MailItemDao::remove(id)) {
|
|
|
|
|
if (item && !item->fileId().isEmpty()) {
|
|
|
|
|
MimeStorageService storage;
|
|
|
|
|
storage.deleteEmlFile(item->fileId());
|
|
|
|
|
}
|
2026-07-05 10:45:44 +02:00
|
|
|
emit mailDeleted(mailItemId);
|
2026-08-17 15:34:49 +02:00
|
|
|
}
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MailService::markAsRead(const QString &mailItemId, bool read)
|
|
|
|
|
{
|
2026-07-05 10:45:44 +02:00
|
|
|
bool ok;
|
|
|
|
|
qint64 id = mailItemId.toLongLong(&ok);
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
std::optional<MailItem> opt = MailItemDao::findById(id);
|
|
|
|
|
if (!opt) return;
|
|
|
|
|
MailItem item = *opt;
|
|
|
|
|
item.setRead(read);
|
|
|
|
|
if (MailItemDao::update(item))
|
|
|
|
|
emit mailReadStateChanged(mailItemId, read);
|
2026-06-17 22:44:27 +02:00
|
|
|
}
|
|
|
|
|
|