feat: IMAP sync incremental + Account Setup Wizard + robust FETCH parser
- ImapSynchronizer::syncFolder(): detecta eliminados (UIDs locales no en servidor), fetch nuevos (sinceUid), actualiza flags (FLAGS batch) - fetchAllUids(): SELECT + SEARCH ALL para lista completa UIDs servidor - parseAndUpdateFlags(): FETCH FLAGS en lotes 100, update DB si cambió read/flagged - AccountSetupDialog: integra ConnectionWizard para IMAP/POP3 con test de conexión real - IMAP FETCH parser robusto: logging respuesta servidor + fallback UID-by-UID - Fix: FETCH failed logging muestra first/last UID + respuesta truncada
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
#include <QSslSocket>
|
||||
#include "db/dao/accountdao.h"
|
||||
#include "db/dao/folderdao.h"
|
||||
#include "db/dao/mailitemdao.h"
|
||||
#include "services/mimestorage.h"
|
||||
#include "core/authenticator.h"
|
||||
#include "core/gmailauthenticator.h"
|
||||
#include "core/outlookauthenticator.h"
|
||||
@@ -79,7 +81,7 @@ void AccountService::addAccount(const Account &account)
|
||||
else if (folderName == "Sent") folder.setSent(true);
|
||||
else if (folderName == "Drafts") folder.setDrafts(true);
|
||||
else if (folderName == "Trash") folder.setTrash(true);
|
||||
if (!FolderDao::insert(folder)) {
|
||||
if (FolderDao::insert(folder) == -1) {
|
||||
qWarning() << "[AccountService] Failed to insert folder" << folder.name()
|
||||
<< "for account ID" << accToAdd.id();
|
||||
}
|
||||
@@ -100,6 +102,12 @@ void AccountService::removeAccount(int accountId)
|
||||
{
|
||||
Account *account = AccountDao::findById(accountId);
|
||||
if (account) {
|
||||
MimeStorageService storage;
|
||||
for (const Folder &folder : FolderDao::findByAccountId(accountId)) {
|
||||
for (const MailItem &item : MailItemDao::findByFolderId(folder.id())) {
|
||||
if (!item.fileId().isEmpty()) storage.deleteEmlFile(item.fileId());
|
||||
}
|
||||
}
|
||||
SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId));
|
||||
AccountDao::remove(accountId);
|
||||
publishAccountEvent(*account, false);
|
||||
@@ -124,10 +132,7 @@ void AccountService::startAuthentication(const QString &email, const QString &pr
|
||||
Authenticator *auth = createAuthenticator(providerType);
|
||||
if (auth) {
|
||||
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
|
||||
addAccount(account.email(),
|
||||
account.type() == AccountType::Gmail ? "gmail"
|
||||
: account.type() == AccountType::Outlook ? "outlook" : "imap",
|
||||
account.accessToken(), account.refreshToken());
|
||||
addAccount(account);
|
||||
});
|
||||
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
|
||||
qWarning() << "[AccountService] Authentication failed:" << error;
|
||||
@@ -239,7 +244,7 @@ void AccountService::syncFoldersForAccount(const Account &account)
|
||||
folder.setAccountId(account.id());
|
||||
// Ensure the folder has an invalid ID (0) so DB assigns new one
|
||||
folder.setId(0);
|
||||
if (!FolderDao::insert(folder)) {
|
||||
if (FolderDao::insert(folder) == -1) {
|
||||
qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr;
|
||||
}
|
||||
}
|
||||
@@ -252,4 +257,4 @@ void AccountService::syncFoldersForAccount(const Account &account)
|
||||
emit accountAdded(account);
|
||||
emit accountListChanged();
|
||||
}
|
||||
#include "accountservice.moc"
|
||||
#include "accountservice.moc"
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
#include "accountservice.h"
|
||||
#include <QDebug>
|
||||
#include <QSslSocket>
|
||||
#include "db/dao/accountdao.h"
|
||||
#include "db/dao/folderdao.h"
|
||||
#include "core/authenticator.h"
|
||||
#include "core/gmailauthenticator.h"
|
||||
#include "core/outlookauthenticator.h"
|
||||
#include "core/imapauthenticator.h"
|
||||
#include "core/synchronizerprovider.h"
|
||||
#include "core/eventbus.h"
|
||||
#include "core/events.h"
|
||||
#include "db/databasemanager.h"
|
||||
#include <QSqlQuery>
|
||||
|
||||
AccountService::AccountService(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QVector<Account> AccountService::getAllAccounts()
|
||||
{
|
||||
return AccountDao::findAll();
|
||||
}
|
||||
|
||||
Account* AccountService::findAccountById(int id)
|
||||
{
|
||||
return AccountDao::findById(id);
|
||||
}
|
||||
|
||||
Account* AccountService::findAccountByEmail(const QString &email)
|
||||
{
|
||||
return AccountDao::findByEmail(email);
|
||||
}
|
||||
|
||||
void AccountService::addAccount(const QString &email, const QString &providerType,
|
||||
const QString &accessToken, const QString &refreshToken)
|
||||
{
|
||||
Account account;
|
||||
account.setEmail(email);
|
||||
account.setDisplayName(email);
|
||||
|
||||
if (providerType == "gmail") account.setType(AccountType::Gmail);
|
||||
else if (providerType == "outlook") account.setType(AccountType::Outlook);
|
||||
else account.setType(AccountType::IMAP);
|
||||
|
||||
account.setAccessToken(accessToken);
|
||||
account.setRefreshToken(refreshToken);
|
||||
|
||||
qint64 id = AccountDao::insert(account);
|
||||
if (id != -1) {
|
||||
account.setId(id);
|
||||
qDebug() << "[AccountService] Account added:" << email;
|
||||
publishAccountEvent(account, true);
|
||||
emit accountAdded(account);
|
||||
emit accountListChanged();
|
||||
} else {
|
||||
qWarning() << "[AccountService] Failed to add account:" << email;
|
||||
}
|
||||
}
|
||||
|
||||
void AccountService::addAccount(const Account &account)
|
||||
{
|
||||
qint64 id = AccountDao::insert(account);
|
||||
if (id != -1) {
|
||||
Account accToAdd = account; // make a copy we can modify
|
||||
accToAdd.setId(id);
|
||||
qDebug() << "[AccountService] Account added:" << accToAdd.email();
|
||||
qDebug() << "[AccountService] Generated ID for new account:" << id;
|
||||
|
||||
// Create default folders for the new account (fallback)
|
||||
QStringList defaultFolderNames = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
|
||||
for (const QString &folderName : defaultFolderNames) {
|
||||
Folder folder;
|
||||
folder.setName(folderName);
|
||||
folder.setAccountId(accToAdd.id());
|
||||
// Set special folder flags
|
||||
if (folderName == "Inbox") folder.setInbox(true);
|
||||
else if (folderName == "Sent") folder.setSent(true);
|
||||
else if (folderName == "Drafts") folder.setDrafts(true);
|
||||
else if (folderName == "Trash") folder.setTrash(true);
|
||||
if (!FolderDao::insert(folder)) {
|
||||
qWarning() << "[AccountService] Failed to insert folder" << folder.name()
|
||||
<< "for account ID" << accToAdd.id();
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to sync real folders from the server; if successful, replace defaults
|
||||
syncFoldersForAccount(accToAdd);
|
||||
|
||||
publishAccountEvent(accToAdd, true);
|
||||
emit accountAdded(accToAdd);
|
||||
emit accountListChanged();
|
||||
} else {
|
||||
qWarning() << "[AccountService] Failed to add account:" << account.email();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountService::removeAccount(int accountId)
|
||||
{
|
||||
Account *account = AccountDao::findById(accountId);
|
||||
if (account) {
|
||||
SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId));
|
||||
AccountDao::remove(accountId);
|
||||
publishAccountEvent(*account, false);
|
||||
emit accountRemoved(accountId);
|
||||
emit accountListChanged();
|
||||
delete account;
|
||||
}
|
||||
}
|
||||
|
||||
void AccountService::updateAccount(const Account &account)
|
||||
{
|
||||
if (AccountDao::update(account)) {
|
||||
qDebug() << "[AccountService] Account updated:" << account.email();
|
||||
// Ensure synchronizer is re‑initialized with new connection settings
|
||||
syncFoldersForAccount(account);
|
||||
emit accountListChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountService::startAuthentication(const QString &email, const QString &providerType)
|
||||
{
|
||||
Authenticator *auth = createAuthenticator(providerType);
|
||||
if (auth) {
|
||||
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
|
||||
addAccount(account.email(),
|
||||
account.type() == AccountType::Gmail ? "gmail"
|
||||
: account.type() == AccountType::Outlook ? "outlook" : "imap",
|
||||
account.accessToken(), account.refreshToken());
|
||||
});
|
||||
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
|
||||
qWarning() << "[AccountService] Authentication failed:" << error;
|
||||
});
|
||||
connect(auth, &QObject::destroyed, auth, [auth]() { /* cleanup handled by Qt */ });
|
||||
auth->authenticate(email);
|
||||
}
|
||||
}
|
||||
|
||||
Authenticator* AccountService::createAuthenticator(const QString &providerType)
|
||||
{
|
||||
if (providerType == "gmail") return new GmailAuthenticator(this);
|
||||
if (providerType == "outlook") return new OutlookAuthenticator(this);
|
||||
if (providerType == "imap") return new ImapAuthenticator(this);
|
||||
qWarning() << "[AccountService] Unknown provider:" << providerType;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool AccountService::testConnection(const Account::ConnectionSettings &settings, QString &errorMessage)
|
||||
{
|
||||
QSslSocket socket;
|
||||
if (settings.incomingSsl) {
|
||||
socket.connectToHostEncrypted(settings.incomingHost, settings.incomingPort);
|
||||
if (!socket.waitForConnected(5000)) {
|
||||
errorMessage = QString("Unable to connect to server %1:%2: %3")
|
||||
.arg(settings.incomingHost)
|
||||
.arg(settings.incomingPort)
|
||||
.arg(socket.errorString());
|
||||
return false;
|
||||
}
|
||||
if (!socket.waitForEncrypted(5000)) {
|
||||
errorMessage = QString("TLS handshake failed: %1").arg(socket.errorString());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
socket.connectToHost(settings.incomingHost, settings.incomingPort);
|
||||
if (!socket.waitForConnected(5000)) {
|
||||
errorMessage = QString("Unable to connect to server %1:%2: %3")
|
||||
.arg(settings.incomingHost)
|
||||
.arg(settings.incomingPort)
|
||||
.arg(socket.errorString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Optionally read greeting (ignore)
|
||||
if (!socket.waitForReadyRead(5000)) {
|
||||
// Some servers may not send greeting until we issue a command; still consider success.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AccountService::publishAccountEvent(const Account &account, bool added)
|
||||
{
|
||||
if (added) {
|
||||
WinoMail::Events::AccountAddedEvent event;
|
||||
event.account = account;
|
||||
EVENT_BUS.publish(event);
|
||||
} else {
|
||||
WinoMail::Events::AccountRemovedEvent event;
|
||||
event.accountId = account.id();
|
||||
EVENT_BUS.publish(event);
|
||||
}
|
||||
}
|
||||
void AccountService::syncFoldersForAccount(const Account &account)
|
||||
{
|
||||
if (account.email().isEmpty()) {
|
||||
qWarning() << "[AccountService] Invalid account for folder sync (empty email)";
|
||||
return;
|
||||
}
|
||||
|
||||
// Only IMAP accounts support folder listing via SYNC
|
||||
if (account.type() != AccountType::IMAP) {
|
||||
qDebug() << QStringLiteral("[AccountService] Folder sync only supported for IMAP accounts; skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
QString accountIdStr = QString::number(account.id());
|
||||
// Get or create synchronizer for this account
|
||||
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountIdStr);
|
||||
if (!sync) {
|
||||
sync = SynchronizerProvider::instance().createSynchronizer(accountIdStr, QStringLiteral("imap"));
|
||||
if (!sync) {
|
||||
qWarning() << QStringLiteral("[AccountService] Failed to create IMAP synchronizer for account") << accountIdStr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Initialize the synchronizer with the account (sets m_account)
|
||||
if (!sync->initialize(account)) {
|
||||
qWarning() << QStringLiteral("[AccountService] Failed to initialize IMAP synchronizer for account") << accountIdStr;
|
||||
// Do not treat as fatal; we can still try to get folders (some impls may not need init)
|
||||
}
|
||||
|
||||
// Retrieve folders from the server
|
||||
QVector<Folder> remoteFolders = sync->getFolders();
|
||||
if (remoteFolders.isEmpty()) {
|
||||
qWarning() << QStringLiteral("[AccountService] No folders returned from IMAP server for account") << accountIdStr;
|
||||
// Optionally, we could keep existing folders; we'll just return and leave default folders as is.
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing folders for this account to avoid duplicates
|
||||
if (!FolderDao::removeByAccountId(account.id())) {
|
||||
qWarning() << QStringLiteral("[AccountService] Failed to remove existing folders for account") << accountIdStr;
|
||||
// Continue anyway; we may end up with duplicates.
|
||||
}
|
||||
|
||||
// Insert each folder from the server
|
||||
for (Folder &folder : remoteFolders) {
|
||||
folder.setAccountId(account.id());
|
||||
// Ensure the folder has an invalid ID (0) so DB assigns new one
|
||||
folder.setId(0);
|
||||
if (!FolderDao::insert(folder)) {
|
||||
qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << QStringLiteral("[AccountService] Synced %1 folders for account %2").arg(remoteFolders.size()).arg(accountIdStr);
|
||||
}
|
||||
|
||||
void AccountService::notifyAccountAdded(const Account &account)
|
||||
{
|
||||
emit accountAdded(account);
|
||||
emit accountListChanged();
|
||||
}
|
||||
#include "accountservice.moc"
|
||||
@@ -11,8 +11,11 @@
|
||||
#include <QDateTime>
|
||||
#include <QTimer>
|
||||
#include <QEventLoop>
|
||||
#include <QNetworkRequest>
|
||||
#include "../../core/events.h"
|
||||
#include "../../core/eventbus.h"
|
||||
#include "db/dao/folderdao.h"
|
||||
#include "services/mimestorage.h"
|
||||
|
||||
GmailSynchronizer::GmailSynchronizer(QObject* parent)
|
||||
: Synchronizer(parent),
|
||||
@@ -84,7 +87,6 @@ bool GmailSynchronizer::syncFolder(const Folder& folder)
|
||||
// En una implementación real, aquí compararíamos con la base de datos local
|
||||
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
|
||||
|
||||
// Por ahora, simulamos que obtenemos algunos elementos
|
||||
if (!items.isEmpty()) {
|
||||
for (const MailItem& item : items) {
|
||||
emit mailItemAdded(item);
|
||||
@@ -142,129 +144,146 @@ QVector<MailItem> GmailSynchronizer::fetchMailItems(const QString& folderId,
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Fetching mail items for folder (label):" << folderId;
|
||||
|
||||
// Construir la URL para Gmail API
|
||||
// Nota: En Gmail, las carpetas se identifican por su nombre de label (ej: INBOX, DRAFTS, etc.)
|
||||
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
|
||||
QString url = buildGmailUrl(endpoint);
|
||||
|
||||
// Parámetros de consulta
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("maxResults", "50"); // Limitar a 50 mensajes por petición
|
||||
query.addQueryItem("q", "in:" + folderId); // Filtrar por label
|
||||
url += "?" + query.toString();
|
||||
|
||||
QNetworkRequest request = createAuthRequest(url);
|
||||
QNetworkReply* reply = m_networkManager->get(request);
|
||||
|
||||
// Nota: En una implementación real, esperaríamos la respuesta asíncronamente
|
||||
// pero por simplicidad en este stub, simulamos una respuesta
|
||||
|
||||
// Simular respuesta para desarrollo
|
||||
qDebug() << "Fetching complete Gmail messages for label:" << folderId;
|
||||
|
||||
QString labelId = folderId;
|
||||
bool localFolderId = false;
|
||||
folderId.toInt(&localFolderId);
|
||||
if (localFolderId) {
|
||||
const auto folder = FolderDao::findById(folderId.toInt());
|
||||
if (folder) {
|
||||
labelId = folder->parentFolderId();
|
||||
if (labelId.isEmpty()) {
|
||||
const QString name = folder->name().toLower();
|
||||
if (name == QStringLiteral("inbox")) labelId = QStringLiteral("INBOX");
|
||||
else if (name == QStringLiteral("sent") || name == QStringLiteral("sent mail")) labelId = QStringLiteral("SENT");
|
||||
else if (name == QStringLiteral("drafts")) labelId = QStringLiteral("DRAFT");
|
||||
else if (name == QStringLiteral("trash")) labelId = QStringLiteral("TRASH");
|
||||
else if (name == QStringLiteral("spam")) labelId = QStringLiteral("SPAM");
|
||||
else labelId = folder->name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QNetworkAccessManager network;
|
||||
auto get = [&](const QUrl &url, QByteArray &data, QString *contentType = nullptr) {
|
||||
QNetworkReply *reply = network.get(createAuthRequest(url.toString()));
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
const bool ok = reply->error() == QNetworkReply::NoError;
|
||||
if (ok) {
|
||||
if (contentType) *contentType = QString::fromLatin1(reply->header(QNetworkRequest::ContentTypeHeader).toByteArray());
|
||||
data = reply->readAll();
|
||||
} else {
|
||||
qWarning() << "Gmail request failed:" << reply->errorString() << url;
|
||||
}
|
||||
reply->deleteLater();
|
||||
return ok;
|
||||
};
|
||||
|
||||
QVector<QJsonObject> messageRefs;
|
||||
QString pageToken;
|
||||
do {
|
||||
QUrl url(buildGmailUrl(QStringLiteral("messages")));
|
||||
QUrlQuery query;
|
||||
query.addQueryItem(QStringLiteral("labelIds"), labelId);
|
||||
query.addQueryItem(QStringLiteral("maxResults"), QStringLiteral("100"));
|
||||
if (!pageToken.isEmpty()) query.addQueryItem(QStringLiteral("pageToken"), pageToken);
|
||||
url.setQuery(query);
|
||||
|
||||
QByteArray response;
|
||||
if (!get(url, response)) return {};
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||
qWarning() << "Invalid Gmail message list:" << parseError.errorString();
|
||||
return {};
|
||||
}
|
||||
const QJsonObject object = document.object();
|
||||
for (const QJsonValue &value : object.value(QStringLiteral("messages")).toArray())
|
||||
if (value.isObject()) messageRefs.append(value.toObject());
|
||||
pageToken = object.value(QStringLiteral("nextPageToken")).toString();
|
||||
} while (!pageToken.isEmpty());
|
||||
|
||||
QVector<MailItem> items;
|
||||
items.append(MailItem(1, folderId.toInt(), "Actualización de proyecto",
|
||||
"carlos.rodriguez@proveedor.com", m_account.email(),
|
||||
QDateTime::currentDateTime().addSecs(-1800),
|
||||
false, false));
|
||||
items.append(MailItem(2, folderId.toInt(), "Factura adjunta",
|
||||
"facturacion@servicios.com", m_account.email(),
|
||||
QDateTime::currentDateTime().addSecs(-5400),
|
||||
true, true)); // Marcada como importante y leída
|
||||
|
||||
MimeStorageService mimeStorage;
|
||||
for (const QJsonObject &reference : messageRefs) {
|
||||
const QString gmailId = reference.value(QStringLiteral("id")).toString();
|
||||
if (gmailId.isEmpty()) continue;
|
||||
QUrl rawUrl(buildGmailUrl(QStringLiteral("messages/%1").arg(gmailId)));
|
||||
QUrlQuery rawQuery;
|
||||
rawQuery.addQueryItem(QStringLiteral("format"), QStringLiteral("raw"));
|
||||
rawUrl.setQuery(rawQuery);
|
||||
QByteArray response;
|
||||
if (!get(rawUrl, response)) continue;
|
||||
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) continue;
|
||||
const QJsonObject object = document.object();
|
||||
QByteArray encoded = object.value(QStringLiteral("raw")).toString().toLatin1();
|
||||
encoded.replace('-', '+');
|
||||
encoded.replace('_', '/');
|
||||
while (encoded.size() % 4) encoded.append('=');
|
||||
const QByteArray rawMime = QByteArray::fromBase64(encoded);
|
||||
ParsedMimeMessage parsed;
|
||||
if (rawMime.isEmpty() || !mimeStorage.parseMessage(rawMime, parsed)) continue;
|
||||
|
||||
MailItem item;
|
||||
item.setFolderId(folderId.toInt());
|
||||
item.setMessageId(gmailId);
|
||||
item.setRawMime(rawMime);
|
||||
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
|
||||
item.setSender(parsed.from);
|
||||
item.setRecipient(parsed.to);
|
||||
item.setTo(parsed.to);
|
||||
item.setCc(parsed.cc);
|
||||
item.setBcc(parsed.bcc);
|
||||
item.setDate(parsed.date);
|
||||
item.setBodyHtml(parsed.bodyHtml);
|
||||
item.setSize(rawMime.size());
|
||||
item.setRead(true);
|
||||
QVector<QString> attachmentNames;
|
||||
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
|
||||
item.setAttachments(attachmentNames);
|
||||
const QJsonArray labels = object.value(QStringLiteral("labelIds")).toArray();
|
||||
for (const QJsonValue &label : labels) {
|
||||
if (label.toString() == QStringLiteral("UNREAD")) item.setRead(false);
|
||||
if (label.toString() == QStringLiteral("STARRED")) item.setFlagged(true);
|
||||
}
|
||||
items.append(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
bool GmailSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||
{
|
||||
if (!m_account.isTokenValid()) {
|
||||
if (!refreshAccessToken()) {
|
||||
qWarning() << "Failed to refresh token for appending mail item";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Appending mail item to folder (label):" << folderId;
|
||||
|
||||
// En una implementación real, llamaríamos a Gmail API
|
||||
// para crear un mensaje en la etiqueta especificada
|
||||
|
||||
// Por ahora, simulamos éxito
|
||||
emit mailItemAdded(item);
|
||||
|
||||
// Publish MailItemAddedEvent
|
||||
WinoMail::Events::MailItemAddedEvent mailEvent;
|
||||
mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
||||
mailEvent.timestamp = QDateTime::currentDateTimeUtc();
|
||||
mailEvent.item = item;
|
||||
PUBLISH(mailEvent);
|
||||
|
||||
return true;
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(item);
|
||||
qWarning() << "Gmail appendMailItem is not used for sending; MailService sends via Gmail API";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GmailSynchronizer::updateMailItemFlags(const QString& folderId,
|
||||
const QString& itemUid,
|
||||
bool read, bool flagged)
|
||||
{
|
||||
if (!m_account.isTokenValid()) {
|
||||
if (!refreshAccessToken()) {
|
||||
qWarning() << "Failed to refresh token for updating mail item flags";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Updating mail item flags:" << itemUid
|
||||
<< "read:" << read << "flagged:" << flagged;
|
||||
|
||||
// En una implementación real, llamaríamos a Gmail API
|
||||
// para actualizar las etiquetas del mensaje (leído, importante, etc.)
|
||||
|
||||
// Por ahora, simulamos éxito
|
||||
MailItem updatedItem;
|
||||
updatedItem.setId(itemUid.toLongLong());
|
||||
updatedItem.setFolderId(folderId.toInt());
|
||||
updatedItem.setRead(read);
|
||||
updatedItem.setFlagged(flagged);
|
||||
emit mailItemUpdated(updatedItem);
|
||||
|
||||
// Publish MailItemUpdatedEvent
|
||||
WinoMail::Events::MailItemUpdatedEvent updateEvent;
|
||||
updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
||||
updateEvent.timestamp = QDateTime::currentDateTimeUtc();
|
||||
updateEvent.item = updatedItem;
|
||||
updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified
|
||||
PUBLISH(updateEvent);
|
||||
|
||||
return true;
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
Q_UNUSED(read);
|
||||
Q_UNUSED(flagged);
|
||||
qWarning() << "Gmail flag update is not available through this synchronizer";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GmailSynchronizer::deleteMailItem(const QString& folderId,
|
||||
const QString& itemUid)
|
||||
{
|
||||
if (!m_account.isTokenValid()) {
|
||||
if (!refreshAccessToken()) {
|
||||
qWarning() << "Failed to refresh token for deleting mail item";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Deleting mail item:" << itemUid << "from folder (label):" << folderId;
|
||||
|
||||
// En una implementación real, llamaríamos a Gmail API
|
||||
// para eliminar el mensaje (moverlo a la papelera)
|
||||
|
||||
// Por ahora, simulamos éxito
|
||||
emit mailItemRemoved(itemUid);
|
||||
|
||||
// Publish MailItemRemovedEvent
|
||||
WinoMail::Events::MailItemRemovedEvent removeEvent;
|
||||
removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
||||
removeEvent.timestamp = QDateTime::currentDateTimeUtc();
|
||||
removeEvent.itemUid = itemUid;
|
||||
removeEvent.folderId = folderId.toInt();
|
||||
PUBLISH(removeEvent);
|
||||
|
||||
return true;
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
qWarning() << "Gmail delete is not available through this synchronizer";
|
||||
return false;
|
||||
}
|
||||
|
||||
void GmailSynchronizer::onGmailReplyFinished(QNetworkReply* reply)
|
||||
@@ -321,9 +340,11 @@ void GmailSynchronizer::onHistoryTimer()
|
||||
|
||||
QString GmailSynchronizer::buildGmailUrl(const QString& endpoint) const
|
||||
{
|
||||
QString normalized = endpoint;
|
||||
while (normalized.startsWith('/')) normalized.remove(0, 1);
|
||||
return QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/%2")
|
||||
.arg(m_account.email())
|
||||
.arg(endpoint);
|
||||
.arg(QString::fromUtf8(QUrl::toPercentEncoding(m_account.email())))
|
||||
.arg(normalized);
|
||||
}
|
||||
|
||||
QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
|
||||
@@ -338,12 +359,41 @@ QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
|
||||
|
||||
bool GmailSynchronizer::refreshAccessToken()
|
||||
{
|
||||
// En una implementación real, aquí usaríamos el refresh token
|
||||
// para obtener un nuevo access token desde Google OAuth2
|
||||
|
||||
qWarning() << "Token refresh not implemented - using stub";
|
||||
// Simulamos éxito por ahora
|
||||
return !m_refreshToken.isEmpty();
|
||||
if (m_refreshToken.isEmpty()) return false;
|
||||
const QString clientId = qEnvironmentVariable("WINO_GMAIL_CLIENT_ID");
|
||||
const QString clientSecret = qEnvironmentVariable("WINO_GMAIL_CLIENT_SECRET");
|
||||
if (clientId.isEmpty() || clientSecret.isEmpty()) {
|
||||
qWarning() << "Gmail token expired and WINO_GMAIL_CLIENT_ID/SECRET are not configured";
|
||||
return false;
|
||||
}
|
||||
|
||||
QNetworkAccessManager network;
|
||||
QNetworkRequest request(QUrl(QStringLiteral("https://oauth2.googleapis.com/token")));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
|
||||
QUrlQuery form;
|
||||
form.addQueryItem(QStringLiteral("client_id"), clientId);
|
||||
form.addQueryItem(QStringLiteral("client_secret"), clientSecret);
|
||||
form.addQueryItem(QStringLiteral("refresh_token"), m_refreshToken);
|
||||
form.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("refresh_token"));
|
||||
QNetworkReply *reply = network.post(request, form.toString(QUrl::FullyEncoded).toUtf8());
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Gmail token refresh failed:" << reply->errorString();
|
||||
reply->deleteLater();
|
||||
return false;
|
||||
}
|
||||
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
|
||||
reply->deleteLater();
|
||||
const QString token = document.object().value(QStringLiteral("access_token")).toString();
|
||||
if (token.isEmpty()) return false;
|
||||
m_accessToken = token;
|
||||
const int expiresIn = document.object().value(QStringLiteral("expires_in")).toInt(3600);
|
||||
m_tokenExpires = QDateTime::currentDateTimeUtc().addSecs(expiresIn);
|
||||
m_account.setAccessToken(m_accessToken);
|
||||
m_account.setTokenExpires(m_tokenExpires);
|
||||
return true;
|
||||
}
|
||||
|
||||
qint64 GmailSynchronizer::getHistoryId(const QString& folderId) const
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
#include "imapconnection.h"
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
|
||||
ImapConnection::ImapConnection(QObject *parent) : QObject(parent),
|
||||
m_socket(new QSslSocket(this)),
|
||||
m_tagCounter(1)
|
||||
{
|
||||
connect(m_socket, &QAbstractSocket::connected, this, &ImapConnection::onConnected);
|
||||
connect(m_socket, &QSslSocket::encrypted, this, &ImapConnection::onEncrypted);
|
||||
m_readyReadConnection = connect(m_socket, &QSslSocket::readyRead, this, &ImapConnection::onReadyRead);
|
||||
connect(m_socket, &QAbstractSocket::errorOccurred, this, &ImapConnection::onSocketError);
|
||||
}
|
||||
|
||||
ImapConnection::~ImapConnection()
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
void ImapConnection::connectToHost(const QString &host, int port, bool useSsl)
|
||||
{
|
||||
m_host = host;
|
||||
m_port = port;
|
||||
m_useSsl = useSsl;
|
||||
if (useSsl) {
|
||||
m_socket->connectToHostEncrypted(host, port);
|
||||
} else {
|
||||
m_socket->connectToHost(host, port);
|
||||
}
|
||||
}
|
||||
|
||||
void ImapConnection::login(const QString &username, const QString &password,
|
||||
bool useAuthPlain,
|
||||
std::function<void(bool, const QString&)> callback)
|
||||
{
|
||||
if (!m_socket->isEncrypted() && m_useSsl) {
|
||||
callback(false, "Not encrypted");
|
||||
return;
|
||||
}
|
||||
QString cmd;
|
||||
if (useAuthPlain) {
|
||||
QByteArray auth;
|
||||
auth.append('\0');
|
||||
auth.append(username.toUtf8());
|
||||
auth.append('\0');
|
||||
auth.append(password.toUtf8());
|
||||
cmd = QString("AUTHENTICATE PLAIN %1").arg(QString::fromLatin1(auth.toBase64()));
|
||||
} else {
|
||||
cmd = QString("LOGIN %1 %2").arg(username, password);
|
||||
}
|
||||
sendCommand(cmd, callback);
|
||||
}
|
||||
|
||||
void ImapConnection::sendCommand(const QString &command,
|
||||
std::function<void(bool, const QString&)> callback)
|
||||
{
|
||||
QString tag = generateTag();
|
||||
QString fullCmd = tag + " " + command + "\r\n";
|
||||
m_socket->write(fullCmd.toUtf8());
|
||||
m_socket->flush();
|
||||
m_pendingCallbacks[tag] = callback;
|
||||
}
|
||||
|
||||
void ImapConnection::disconnect()
|
||||
{
|
||||
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
|
||||
m_socket->disconnectFromHost();
|
||||
m_socket->waitForDisconnected(1000);
|
||||
}
|
||||
m_pendingCallbacks.clear();
|
||||
}
|
||||
|
||||
void ImapConnection::onConnected()
|
||||
{
|
||||
// For non-SSL connections, signal connected immediately.
|
||||
// For SSL connections, onEncrypted() fires instead after the SSL handshake.
|
||||
if (!m_useSsl) {
|
||||
emit connected();
|
||||
}
|
||||
}
|
||||
|
||||
void ImapConnection::onEncrypted()
|
||||
{
|
||||
emit connected();
|
||||
}
|
||||
|
||||
void ImapConnection::onReadyRead()
|
||||
{
|
||||
while (m_socket->canReadLine()) {
|
||||
QString line = QString::fromUtf8(m_socket->readLine()).trimmed();
|
||||
processLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
void ImapConnection::onSocketError(QAbstractSocket::SocketError error)
|
||||
{
|
||||
emit errorOccurred(m_socket->errorString());
|
||||
}
|
||||
|
||||
void ImapConnection::processLine(const QString &line)
|
||||
{
|
||||
// If it's an untagged response (starts with *), emit signal
|
||||
if (line.startsWith('*')) {
|
||||
emit untaggedResponse(line);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the tag prefix (first word) and look it up in pending callbacks
|
||||
int spacePos = line.indexOf(' ');
|
||||
if (spacePos == -1) return; // malformed line
|
||||
|
||||
QString tagFromLine = line.left(spacePos);
|
||||
|
||||
auto it = m_pendingCallbacks.find(tagFromLine);
|
||||
if (it != m_pendingCallbacks.end()) {
|
||||
bool ok = line.contains(" OK ");
|
||||
auto cb = it.value();
|
||||
m_pendingCallbacks.erase(it);
|
||||
cb(ok, line);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here, it's an unhandled response
|
||||
qWarning() << "Unhandled IMAP response:" << line;
|
||||
}
|
||||
|
||||
void ImapConnection::sendRaw(const QString &data)
|
||||
{
|
||||
m_socket->write(data.toUtf8() + "\r\n");
|
||||
m_socket->flush();
|
||||
}
|
||||
|
||||
QString ImapConnection::generateTag()
|
||||
{
|
||||
// Simple tag: A001, A002, ... (like traditional IMAP clients)
|
||||
return QString("A%1").arg(m_tagCounter++, 4, 10, QChar('0'));
|
||||
}
|
||||
|
||||
bool ImapConnection::sendCommandWait(const QString &command, QString &response, int msecs)
|
||||
{
|
||||
QByteArray rawResponse;
|
||||
const bool completed = sendCommandWaitBytes(command, rawResponse, msecs);
|
||||
response = QString::fromUtf8(rawResponse);
|
||||
return completed;
|
||||
}
|
||||
|
||||
bool ImapConnection::sendCommandWaitBytes(const QString &command, QByteArray &response, int msecs)
|
||||
{
|
||||
QString tag = generateTag();
|
||||
QString fullCmd = tag + " " + command + "\r\n";
|
||||
bool complete = false;
|
||||
bool timedOut = false;
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
QByteArray buffer;
|
||||
const QByteArray tagBytes = tag.toUtf8();
|
||||
|
||||
// Fetch responses contain arbitrary MIME bytes and IMAP literals. The
|
||||
// line-based parser cannot safely consume those bytes, so temporarily
|
||||
// take ownership of readyRead and collect the complete tagged response.
|
||||
QObject::disconnect(m_readyReadConnection);
|
||||
auto collectResponse = [&]() {
|
||||
buffer.append(m_socket->readAll());
|
||||
|
||||
int pos = 0;
|
||||
while (pos < buffer.size()) {
|
||||
const int lineEnd = buffer.indexOf("\r\n", pos);
|
||||
if (lineEnd < 0) break;
|
||||
const QByteArray line = buffer.mid(pos, lineEnd - pos);
|
||||
const int open = line.lastIndexOf('{');
|
||||
QByteArray literalSize = open >= 0 && line.endsWith('}')
|
||||
? line.mid(open + 1, line.size() - open - 2) : QByteArray();
|
||||
if (literalSize.endsWith('+')) literalSize.chop(1);
|
||||
bool literalOk = false;
|
||||
const qint64 parsedLiteralSize = literalSize.toLongLong(&literalOk);
|
||||
const bool hasLiteral = open >= 0 && line.endsWith('}') && literalOk && parsedLiteralSize >= 0;
|
||||
if (hasLiteral) {
|
||||
const qint64 length = parsedLiteralSize;
|
||||
const int dataStart = lineEnd + 2;
|
||||
if (buffer.size() < dataStart + length) break;
|
||||
pos = dataStart + int(length);
|
||||
if (buffer.mid(pos, 2) == QByteArrayLiteral("\r\n")) pos += 2;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith(tagBytes + QByteArrayLiteral(" "))) {
|
||||
complete = true;
|
||||
loop.quit();
|
||||
break;
|
||||
}
|
||||
pos = lineEnd + 2;
|
||||
}
|
||||
};
|
||||
QMetaObject::Connection readyConn = connect(m_socket, &QSslSocket::readyRead, &loop, collectResponse);
|
||||
QMetaObject::Connection errorConn = connect(m_socket, &QAbstractSocket::errorOccurred, &loop, [&](QAbstractSocket::SocketError) {
|
||||
loop.quit();
|
||||
});
|
||||
timer.setSingleShot(true);
|
||||
QMetaObject::Connection timeoutConn = connect(&timer, &QTimer::timeout, &loop, [&]() {
|
||||
timedOut = true;
|
||||
loop.quit();
|
||||
});
|
||||
|
||||
m_socket->write(fullCmd.toUtf8());
|
||||
m_socket->flush();
|
||||
if (m_socket->bytesAvailable() > 0) collectResponse();
|
||||
timer.start(msecs);
|
||||
loop.exec();
|
||||
QObject::disconnect(readyConn);
|
||||
QObject::disconnect(errorConn);
|
||||
QObject::disconnect(timeoutConn);
|
||||
m_readyReadConnection = connect(m_socket, &QSslSocket::readyRead, this, &ImapConnection::onReadyRead);
|
||||
|
||||
response = buffer;
|
||||
if (timedOut || !complete) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#ifndef IMAPCONNECTION_H
|
||||
#define IMAPCONNECTION_H
|
||||
|
||||
#include <QSslSocket>
|
||||
#include <QObject>
|
||||
#include <functional>
|
||||
#include <QMap>
|
||||
#include <QByteArray>
|
||||
|
||||
class ImapConnection : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ImapConnection(QObject *parent = nullptr);
|
||||
~ImapConnection();
|
||||
|
||||
void connectToHost(const QString &host, int port, bool useSsl);
|
||||
void login(const QString &username, const QString &password,
|
||||
bool useAuthPlain,
|
||||
std::function<void(bool, const QString&)> callback);
|
||||
void sendCommand(const QString &command,
|
||||
std::function<void(bool, const QString&)> callback);
|
||||
void disconnect();
|
||||
|
||||
// Synchronous version: blocks until response or timeout
|
||||
bool sendCommandWait(const QString &command, QString &response, int msecs);
|
||||
bool sendCommandWaitBytes(const QString &command, QByteArray &response, int msecs);
|
||||
|
||||
signals:
|
||||
void connected();
|
||||
void disconnected();
|
||||
void errorOccurred(const QString &message);
|
||||
void untaggedResponse(const QString &line); // for * RESPONSES
|
||||
void ready(); // connection ready for authentication (TCP connected for plain, encrypted for SSL)
|
||||
|
||||
private slots:
|
||||
void onConnected();
|
||||
void onEncrypted();
|
||||
void onReadyRead();
|
||||
void onSocketError(QAbstractSocket::SocketError error);
|
||||
|
||||
private:
|
||||
QSslSocket *m_socket;
|
||||
QString m_host;
|
||||
int m_port;
|
||||
bool m_useSsl;
|
||||
QString m_username;
|
||||
QString m_password;
|
||||
int m_tagCounter;
|
||||
QString m_responseBuffer;
|
||||
QMap<QString, std::function<void(bool, const QString&)>> m_pendingCallbacks; // keyed by tag
|
||||
QMetaObject::Connection m_readyReadConnection;
|
||||
|
||||
void processLine(const QString &line);
|
||||
QString generateTag();
|
||||
public:
|
||||
void sendRaw(const QString &data);
|
||||
};
|
||||
|
||||
#endif // IMAPCONNECTION_H
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "../../core/models/folder.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
#include "../../services/mimestorage.h"
|
||||
#include <algorithm>
|
||||
|
||||
ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
||||
: Synchronizer(parent)
|
||||
@@ -16,6 +18,7 @@ ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
||||
|
||||
bool ImapSynchronizer::initialize(const Account& account)
|
||||
{
|
||||
m_account = account;
|
||||
const Account::ConnectionSettings& settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
@@ -37,9 +40,13 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
||||
QEventLoop connectLoop;
|
||||
QTimer connectTimer;
|
||||
bool connectTimeout = false;
|
||||
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
|
||||
connect(&conn, &ImapConnection::connected, &connectLoop, [&]() {
|
||||
connected = true;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP connection error:" << err;
|
||||
connected = false;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.setSingleShot(true);
|
||||
@@ -54,7 +61,7 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
||||
qWarning() << "Connection timeout to" << m_host;
|
||||
return false;
|
||||
}
|
||||
connected = true;
|
||||
if (!connected) return false;
|
||||
|
||||
// Wait for server greeting (first untagged response)
|
||||
QEventLoop greetLoop;
|
||||
@@ -114,99 +121,173 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::syncFolder(const Folder& folder)
|
||||
QVector<qint64> ImapSynchronizer::parseUidSearchResponse(const QString& response) const
|
||||
{
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// Select folder
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
|
||||
QString selectResp;
|
||||
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed for" << folder.name();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all UIDs
|
||||
QString searchCmd = "UID SEARCH ALL";
|
||||
QString searchResp;
|
||||
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
|
||||
qWarning() << "SEARCH failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResp.split('\n');
|
||||
QStringList lines = response.split(QStringLiteral("\n"));
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
if (line.startsWith(QStringLiteral("* SEARCH"))) {
|
||||
QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
if (ok && uid > 0)
|
||||
uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no messages, set unread count to 0 and exit
|
||||
if (uids.isEmpty()) {
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(0);
|
||||
FolderDao::update(f);
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get flags for all UIDs in one command
|
||||
QStringList uidStrs;
|
||||
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
|
||||
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
|
||||
QString fetchResp;
|
||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
|
||||
qWarning() << "FETCH FLAGS failed";
|
||||
conn.disconnect();
|
||||
return uids;
|
||||
}
|
||||
bool ImapSynchronizer::syncFolder(const Folder& folder)
|
||||
{
|
||||
// First, get all UIDs from server to detect deletions
|
||||
QVector<qint64> allUids = fetchAllUids(QString::number(folder.id()));
|
||||
if (allUids.isEmpty()) {
|
||||
qWarning() << "Failed to fetch UIDs for folder" << folder.id();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse flags and update local DB
|
||||
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
|
||||
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
|
||||
auto it = flagsRx.globalMatch(fetchResp);
|
||||
while (it.hasNext()) {
|
||||
auto m = it.next();
|
||||
qint64 uid = m.captured(1).toLongLong();
|
||||
QString flags = m.captured(2);
|
||||
bool seen = flags.contains("\\Seen");
|
||||
bool flagged = flags.contains("\\Flagged");
|
||||
flagsMap[uid] = qMakePair(seen, flagged);
|
||||
// Get local UIDs
|
||||
QVector<qint64> localUids = MailItemDao::getUidsForFolder(folder.id());
|
||||
|
||||
// Find deleted UIDs (exist locally but not on server)
|
||||
QSet<qint64> serverUidSet(allUids.begin(), allUids.end());
|
||||
QVector<qint64> deletedUids;
|
||||
for (qint64 uid : localUids) {
|
||||
if (!serverUidSet.contains(uid)) {
|
||||
deletedUids.append(uid);
|
||||
}
|
||||
}
|
||||
|
||||
// Update local items
|
||||
auto localItems = MailItemDao::findByFolderId(folder.id());
|
||||
for (MailItem& item : localItems) {
|
||||
if (flagsMap.contains(item.uid())) {
|
||||
bool seen = flagsMap[item.uid()].first;
|
||||
bool flagged = flagsMap[item.uid()].second;
|
||||
if (item.isRead() != seen || item.isFlagged() != flagged) {
|
||||
item.setRead(seen);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
}
|
||||
|
||||
// Delete locally if removed from server
|
||||
bool success = true;
|
||||
for (qint64 uid : deletedUids) {
|
||||
if (!MailItemDao::removeByUid(folder.id(), uid)) {
|
||||
qWarning() << "Failed to delete local mail item UID" << uid;
|
||||
success = false;
|
||||
} else {
|
||||
qDebug() << "Removed local mail item UID" << uid << "(deleted on server)";
|
||||
}
|
||||
}
|
||||
|
||||
// Count unread
|
||||
int unread = 0;
|
||||
for (const MailItem& item : localItems) {
|
||||
if (!item.isRead()) unread++;
|
||||
// Now fetch new/changed messages
|
||||
const qint64 sinceUid = MailItemDao::maxUidForFolder(folder.id()).value_or(0);
|
||||
QVector<MailItem> items = fetchMailItems(QString::number(folder.id()), sinceUid);
|
||||
for (MailItem &item : items) {
|
||||
if (!persistFetchedItem(folder, item)) success = false;
|
||||
}
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(unread);
|
||||
FolderDao::update(f);
|
||||
|
||||
// Also check for flag changes on existing messages
|
||||
if (!allUids.isEmpty()) {
|
||||
// Fetch flags for all known UIDs (in batches)
|
||||
const int batchSize = 100;
|
||||
for (int i = 0; i < allUids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = allUids.mid(i, qMin(batchSize, allUids.size() - i));
|
||||
QStringList batchList;
|
||||
for (qint64 uid : batch) batchList << QString::number(uid);
|
||||
QString uidList = batchList.join(",");
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) continue;
|
||||
|
||||
// SELECT folder
|
||||
QString selectCommand = QString("SELECT \"%1\"").arg(folder.name());
|
||||
QString selectResponse;
|
||||
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch only FLAGS for this batch
|
||||
QString fetchCommand = QString("UID FETCH %1 (FLAGS)").arg(uidList);
|
||||
QByteArray fetchResponse;
|
||||
if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse flags and update local
|
||||
parseAndUpdateFlags(fetchResponse, folder.id());
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
QVector<qint64> ImapSynchronizer::fetchAllUids(const QString& folderId) const
|
||||
{
|
||||
QVector<qint64> uids;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return uids;
|
||||
}
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return uids;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// SELECT
|
||||
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString selectResponse;
|
||||
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed:" << selectResponse;
|
||||
return uids;
|
||||
}
|
||||
|
||||
// SEARCH ALL
|
||||
QString searchCommand = "UID SEARCH ALL";
|
||||
QString searchResponse;
|
||||
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
|
||||
qWarning() << "SEARCH command failed:" << searchResponse;
|
||||
return uids;
|
||||
}
|
||||
|
||||
uids = parseUidSearchResponse(searchResponse);
|
||||
return uids;
|
||||
}
|
||||
|
||||
void ImapSynchronizer::parseAndUpdateFlags(const QByteArray& response, int folderId) const
|
||||
{
|
||||
// Parse FETCH response for FLAGS
|
||||
QStringList lines = QString::fromUtf8(response).split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (!line.startsWith("* ") || !line.contains(" FETCH ")) continue;
|
||||
|
||||
// Extract UID
|
||||
QRegularExpression uidRx(QStringLiteral("\\bUID\\s+(\\d+)"));
|
||||
QRegularExpressionMatch uidMatch = uidRx.match(line);
|
||||
if (!uidMatch.hasMatch()) continue;
|
||||
qint64 uid = uidMatch.captured(1).toLongLong();
|
||||
|
||||
// Extract FLAGS
|
||||
int flagsPos = line.indexOf("FLAGS (");
|
||||
if (flagsPos < 0) continue;
|
||||
int flagsEnd = line.indexOf(')', flagsPos);
|
||||
if (flagsEnd < 0) continue;
|
||||
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
||||
|
||||
bool read = flags.contains("\\Seen");
|
||||
bool flagged = flags.contains("\\Flagged");
|
||||
|
||||
// Update local DB if different
|
||||
auto itemOpt = MailItemDao::findByUid(folderId, uid);
|
||||
if (itemOpt.has_value()) {
|
||||
MailItem item = itemOpt.value();
|
||||
if (item.isRead() != read || item.isFlagged() != flagged) {
|
||||
item.setRead(read);
|
||||
item.setFlagged(flagged);
|
||||
if (!MailItemDao::update(item)) {
|
||||
qWarning() << "Failed to update flags for UID" << uid;
|
||||
} else {
|
||||
qDebug() << "Updated flags for UID" << uid << "read=" << read << "flagged=" << flagged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
|
||||
@@ -242,7 +323,7 @@ QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint
|
||||
// SEARCH
|
||||
QString searchCommand;
|
||||
if (sinceUid > 0) {
|
||||
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
|
||||
searchCommand = QString("UID SEARCH UID %1:*").arg(sinceUid + 1);
|
||||
} else {
|
||||
searchCommand = "UID SEARCH ALL";
|
||||
}
|
||||
@@ -283,24 +364,38 @@ QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint
|
||||
}
|
||||
batchList.chop(1); // remove trailing comma
|
||||
|
||||
// Fetch headers and flags (efficient)
|
||||
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
|
||||
// Fetch the complete RFC822 message. Headers alone are not enough to
|
||||
// reconstruct HTML, inline resources or attachments.
|
||||
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)")
|
||||
.arg(batchList);
|
||||
QString fetchResponse;
|
||||
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
|
||||
qWarning() << "FETCH failed for batch" << i;
|
||||
QByteArray fetchResponse;
|
||||
if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) {
|
||||
qWarning() << "FETCH failed for batch" << i << "(" << batch.size() << "UIDs)"
|
||||
<< "- first UID:" << batch.first() << "last UID:" << batch.last()
|
||||
<< "- response:" << QString::fromUtf8(fetchResponse).left(200);
|
||||
// Fallback: try fetching UIDs one by one
|
||||
for (qint64 uid : batch) {
|
||||
QString singleFetchCmd = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)").arg(uid);
|
||||
QByteArray singleResponse;
|
||||
if (conn.sendCommandWaitBytes(singleFetchCmd, singleResponse, 30000)) {
|
||||
QVector<MailItem> singleItems = parseFetchResponseBytes(singleResponse);
|
||||
for (MailItem& item : singleItems) {
|
||||
item.setFolderId(fid);
|
||||
items.append(item);
|
||||
}
|
||||
} else {
|
||||
qWarning() << " Single UID fetch failed for UID" << uid
|
||||
<< "- response:" << QString::fromUtf8(singleResponse).left(200);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse and persist
|
||||
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
|
||||
QVector<MailItem> batchItems = parseFetchResponseBytes(fetchResponse);
|
||||
for (MailItem& item : batchItems) {
|
||||
item.setFolderId(fid);
|
||||
if (MailItemDao::insert(item)) {
|
||||
items.append(item);
|
||||
} else {
|
||||
qWarning() << "Failed to insert mail item uid" << item.uid();
|
||||
}
|
||||
items.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +432,132 @@ QVector<Folder> ImapSynchronizer::getFolders() const
|
||||
return folders;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::persistFetchedItem(const Folder& folder, MailItem& item) const
|
||||
{
|
||||
MimeStorageService storage;
|
||||
QVector<QString> storedPaths;
|
||||
ParsedMimeMessage parsed;
|
||||
if (!item.rawMime().isEmpty()) {
|
||||
if (!storage.parseMessage(item.rawMime(), parsed)
|
||||
|| !storage.storeMessage(QString::number(m_account.id()), QString::number(folder.id()),
|
||||
item, item.rawMime(), &storedPaths)) {
|
||||
qWarning() << "Failed to store MIME message for UID" << item.uid();
|
||||
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 &attachment = parsed.attachments.at(i);
|
||||
StoredAttachmentRecord record;
|
||||
record.fileName = attachment.fileName;
|
||||
record.mimeType = attachment.mimeType;
|
||||
record.contentId = attachment.contentId;
|
||||
record.size = attachment.data.size();
|
||||
if (i < storedPaths.size()) record.storedPath = storedPaths.at(i);
|
||||
records.append(record);
|
||||
}
|
||||
if (!MailItemDao::replaceAttachments(item.id(), records)) {
|
||||
storage.deleteEmlFile(item.fileId());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::parseFetchResponseBytes(const QByteArray& response) const
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
MimeStorageService mimeStorage;
|
||||
int searchFrom = 0;
|
||||
|
||||
// Debug: check if response contains error
|
||||
if (response.contains(" NO ") || response.contains(" BAD ")) {
|
||||
qWarning() << "IMAP FETCH response contains error:" << QString::fromUtf8(response).left(500);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const int bodyPos = response.indexOf("BODY[]", searchFrom);
|
||||
if (bodyPos < 0) {
|
||||
// Check if we have untagged FETCH responses without BODY[]
|
||||
int fetchPos = response.indexOf(" FETCH ", searchFrom);
|
||||
if (fetchPos >= 0) {
|
||||
int lineEnd = response.indexOf("\r\n", fetchPos);
|
||||
if (lineEnd > 0) {
|
||||
QByteArray line = response.mid(fetchPos, lineEnd - fetchPos);
|
||||
qWarning() << "FETCH response without BODY[]:" << QString::fromLatin1(line);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
const int bracePos = response.indexOf('{', bodyPos);
|
||||
const int braceEnd = response.indexOf('}', bracePos);
|
||||
if (bracePos < 0 || braceEnd < 0) break;
|
||||
QByteArray literalSize = response.mid(bracePos + 1, braceEnd - bracePos - 1);
|
||||
if (literalSize.endsWith('+')) literalSize.chop(1);
|
||||
bool sizeOk = false;
|
||||
const qint64 size = literalSize.toLongLong(&sizeOk);
|
||||
const int lineEnd = response.indexOf("\r\n", braceEnd);
|
||||
if (!sizeOk || lineEnd < 0 || response.size() < lineEnd + 2 + size) {
|
||||
qWarning() << "FETCH parse error: sizeOk=" << sizeOk << "lineEnd=" << lineEnd
|
||||
<< "response.size=" << response.size() << "needed=" << (lineEnd + 2 + size);
|
||||
break;
|
||||
}
|
||||
|
||||
const QByteArray rawMime = response.mid(lineEnd + 2, int(size));
|
||||
const int fetchLineStart = response.lastIndexOf("\r\n", bodyPos) + 2;
|
||||
const QByteArray fetchLine = response.mid(fetchLineStart, bodyPos - fetchLineStart);
|
||||
const QRegularExpression uidRx(QStringLiteral("\\bUID\\s+(\\d+)"));
|
||||
const QRegularExpressionMatch uidMatch = uidRx.match(QString::fromLatin1(fetchLine));
|
||||
if (!uidMatch.hasMatch()) {
|
||||
qWarning() << "No UID found in FETCH line:" << QString::fromLatin1(fetchLine).left(200);
|
||||
searchFrom = lineEnd + 2 + int(size);
|
||||
continue;
|
||||
}
|
||||
|
||||
ParsedMimeMessage parsed;
|
||||
if (mimeStorage.parseMessage(rawMime, parsed)) {
|
||||
MailItem item;
|
||||
item.setUid(uidMatch.captured(1).toLongLong());
|
||||
item.setMessageId(parsed.messageId);
|
||||
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
|
||||
item.setSender(parsed.from);
|
||||
item.setRecipient(parsed.to);
|
||||
item.setTo(parsed.to);
|
||||
item.setCc(parsed.cc);
|
||||
item.setBcc(parsed.bcc);
|
||||
item.setDate(parsed.date);
|
||||
item.setBodyHtml(parsed.bodyHtml);
|
||||
item.setSize(rawMime.size());
|
||||
item.setRawMime(rawMime);
|
||||
QVector<QString> names;
|
||||
for (const ParsedMimeAttachment &attachment : parsed.attachments)
|
||||
names.append(attachment.fileName);
|
||||
item.setAttachments(names);
|
||||
|
||||
// Flags are in the FETCH line, not in the MIME payload.
|
||||
const int flagsPos = fetchLine.indexOf("FLAGS (");
|
||||
if (flagsPos >= 0) {
|
||||
const int flagsEnd = fetchLine.indexOf(')', flagsPos);
|
||||
if (flagsEnd > flagsPos) {
|
||||
const QString flags = QString::fromLatin1(fetchLine.mid(flagsPos + 7, flagsEnd - flagsPos - 7));
|
||||
item.setRead(flags.contains(QStringLiteral("\\Seen")));
|
||||
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
|
||||
}
|
||||
}
|
||||
items.append(item);
|
||||
} else {
|
||||
qWarning() << "Failed to parse MIME for UID" << uidMatch.captured(1);
|
||||
}
|
||||
searchFrom = lineEnd + 2 + int(size);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||
{
|
||||
// Get folder name
|
||||
@@ -562,19 +783,20 @@ QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines)
|
||||
int flagsEnd = line.indexOf(')', flagsPos + 7);
|
||||
if (flagsEnd != -1) {
|
||||
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
||||
item.setRead(flags.contains(QStringLiteral("\\\\Seen")));
|
||||
item.setFlagged(flags.contains(QStringLiteral("\\\\Flagged")));
|
||||
item.setRead(flags.contains(QStringLiteral("\\Seen")));
|
||||
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract INTERNALDATE
|
||||
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"\""));
|
||||
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
|
||||
if (datePos != -1) {
|
||||
int dateStart = datePos + 16; // length of "INTERNALDATE \""
|
||||
int dateStart = datePos + 15; // length of "INTERNALDATE \""
|
||||
int dateEnd = line.indexOf('\"', dateStart);
|
||||
if (dateEnd != -1) {
|
||||
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
|
||||
QDateTime dt = QDateTime::fromString(dateStr, QStringLiteral("dd-MMM-yyyy hh:mm:ss zzz"));
|
||||
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
|
||||
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
@@ -582,7 +804,7 @@ QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines)
|
||||
|
||||
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
|
||||
QString bodyData;
|
||||
int bodyPos = line.indexOf(QStringLiteral("BODY["));
|
||||
int bodyPos = line.indexOf(QStringLiteral("BODY[]"));
|
||||
if (bodyPos != -1) {
|
||||
int bracePos = line.indexOf('{', bodyPos);
|
||||
if (bracePos != -1) {
|
||||
|
||||
@@ -0,0 +1,649 @@
|
||||
#include "imapsynchronizer.h"
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include "../../core/mailitem.h"
|
||||
#include "../../core/models/account.h"
|
||||
#include "../../core/models/folder.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
|
||||
ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
||||
: Synchronizer(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::initialize(const Account& account)
|
||||
{
|
||||
const Account::ConnectionSettings& settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
m_useSsl = settings.incomingSsl;
|
||||
m_username = settings.username;
|
||||
m_password = settings.password;
|
||||
m_authMethod = settings.authMethod;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: connects and logs in to the IMAP server
|
||||
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
||||
{
|
||||
bool connected = false;
|
||||
bool loggedIn = false;
|
||||
QString response;
|
||||
|
||||
// Connect to host
|
||||
QEventLoop connectLoop;
|
||||
QTimer connectTimer;
|
||||
bool connectTimeout = false;
|
||||
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP connection error:" << err;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.setSingleShot(true);
|
||||
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
|
||||
connectTimeout = true;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.start(30000); // 30 seconds
|
||||
conn.connectToHost(m_host, m_port, m_useSsl);
|
||||
connectLoop.exec();
|
||||
if (connectTimeout) {
|
||||
qWarning() << "Connection timeout to" << m_host;
|
||||
return false;
|
||||
}
|
||||
connected = true;
|
||||
|
||||
// Wait for server greeting (first untagged response)
|
||||
QEventLoop greetLoop;
|
||||
bool gotGreeting = false;
|
||||
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
|
||||
[&](const QString& line) {
|
||||
Q_UNUSED(line);
|
||||
gotGreeting = true;
|
||||
greetLoop.quit();
|
||||
});
|
||||
QTimer greetTimer;
|
||||
greetTimer.setSingleShot(true);
|
||||
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
|
||||
greetLoop.quit();
|
||||
});
|
||||
greetTimer.start(30000); // 30 seconds
|
||||
greetLoop.exec();
|
||||
QObject::disconnect(greetConn);
|
||||
if (!gotGreeting) {
|
||||
qWarning() << "Timeout waiting for server greeting";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Login
|
||||
QEventLoop loginLoop;
|
||||
QTimer loginTimer;
|
||||
bool loginTimeout = false;
|
||||
bool loginSuccess = false;
|
||||
// Already connected, now login
|
||||
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
|
||||
if (ok) {
|
||||
loginSuccess = true;
|
||||
} else {
|
||||
qWarning() << "Login failed:" << resp;
|
||||
}
|
||||
loginLoop.quit();
|
||||
});
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP error during login:" << err;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.setSingleShot(true);
|
||||
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
|
||||
loginTimeout = true;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.start(30000);
|
||||
loginLoop.exec();
|
||||
if (loginTimeout) {
|
||||
qWarning() << "Login timeout";
|
||||
return false;
|
||||
}
|
||||
if (!loginSuccess) {
|
||||
return false;
|
||||
}
|
||||
loggedIn = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::syncFolder(const Folder& folder)
|
||||
{
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// Select folder
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
|
||||
QString selectResp;
|
||||
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed for" << folder.name();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all UIDs
|
||||
QString searchCmd = "UID SEARCH ALL";
|
||||
QString searchResp;
|
||||
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
|
||||
qWarning() << "SEARCH failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResp.split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no messages, set unread count to 0 and exit
|
||||
if (uids.isEmpty()) {
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(0);
|
||||
FolderDao::update(f);
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get flags for all UIDs in one command
|
||||
QStringList uidStrs;
|
||||
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
|
||||
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
|
||||
QString fetchResp;
|
||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
|
||||
qWarning() << "FETCH FLAGS failed";
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse flags and update local DB
|
||||
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
|
||||
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
|
||||
auto it = flagsRx.globalMatch(fetchResp);
|
||||
while (it.hasNext()) {
|
||||
auto m = it.next();
|
||||
qint64 uid = m.captured(1).toLongLong();
|
||||
QString flags = m.captured(2);
|
||||
bool seen = flags.contains("\\Seen");
|
||||
bool flagged = flags.contains("\\Flagged");
|
||||
flagsMap[uid] = qMakePair(seen, flagged);
|
||||
}
|
||||
|
||||
// Update local items
|
||||
auto localItems = MailItemDao::findByFolderId(folder.id());
|
||||
for (MailItem& item : localItems) {
|
||||
if (flagsMap.contains(item.uid())) {
|
||||
bool seen = flagsMap[item.uid()].first;
|
||||
bool flagged = flagsMap[item.uid()].second;
|
||||
if (item.isRead() != seen || item.isFlagged() != flagged) {
|
||||
item.setRead(seen);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count unread
|
||||
int unread = 0;
|
||||
for (const MailItem& item : localItems) {
|
||||
if (!item.isRead()) unread++;
|
||||
}
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(unread);
|
||||
FolderDao::update(f);
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Get folder name from DB
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return items;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// SELECT with real folder name
|
||||
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString selectResponse;
|
||||
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
|
||||
qWarning() << "SELECT command failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
if (!selectResponse.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// SEARCH
|
||||
QString searchCommand;
|
||||
if (sinceUid > 0) {
|
||||
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
|
||||
} else {
|
||||
searchCommand = "UID SEARCH ALL";
|
||||
}
|
||||
QString searchResponse;
|
||||
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
|
||||
qWarning() << "SEARCH command failed:" << searchResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// Parse UIDs (robust: look for line starting with "* SEARCH")
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResponse.split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uids.isEmpty()) {
|
||||
conn.sendCommandWait("CLOSE", searchResponse, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
// Fetch in batches of 50 UIDs
|
||||
const int batchSize = 50;
|
||||
for (int i = 0; i < uids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = uids.mid(i, qMin(batchSize, uids.size() - i));
|
||||
QString batchList;
|
||||
for (qint64 uid : batch) {
|
||||
batchList.append(QString::number(uid)).append(",");
|
||||
}
|
||||
batchList.chop(1); // remove trailing comma
|
||||
|
||||
// Fetch headers and flags (efficient)
|
||||
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
|
||||
.arg(batchList);
|
||||
QString fetchResponse;
|
||||
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
|
||||
qWarning() << "FETCH failed for batch" << i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse and persist
|
||||
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
|
||||
for (MailItem& item : batchItems) {
|
||||
item.setFolderId(fid);
|
||||
if (MailItemDao::insert(item)) {
|
||||
items.append(item);
|
||||
} else {
|
||||
qWarning() << "Failed to insert mail item uid" << item.uid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close and disconnect
|
||||
QString dummy;
|
||||
conn.sendCommandWait("CLOSE", dummy, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::getFolders() const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return folders;
|
||||
}
|
||||
|
||||
// List folders: LIST "" "*"
|
||||
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(listCommand, response, 30000)) {
|
||||
qWarning() << "LIST command failed:" << response;
|
||||
return folders;
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
QStringList lines = response.split('\n');
|
||||
folders = parseListResponse(lines);
|
||||
|
||||
// Logout
|
||||
conn.disconnect();
|
||||
return folders;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||
{
|
||||
// Get folder name
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return false;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// Build RFC822 message (headers + body)
|
||||
QStringList headers;
|
||||
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
|
||||
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
|
||||
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
|
||||
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
|
||||
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
|
||||
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
|
||||
headers << "MIME-Version: 1.0";
|
||||
headers << "Content-Type: text/html; charset=UTF-8";
|
||||
headers << "Content-Transfer-Encoding: 7bit";
|
||||
headers << ""; // blank line separates headers from body
|
||||
headers << item.bodyHtml(); // using bodyHtml as the body
|
||||
|
||||
QString message = headers.join("\r\n");
|
||||
QByteArray msgData = message.toUtf8();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT folder (some servers require it)
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
|
||||
|
||||
// APPEND with literal: APPEND "folder" (\Seen) {size}
|
||||
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
|
||||
qWarning() << "APPEND command initial failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the literal (message data)
|
||||
conn.sendRaw(QString::fromUtf8(msgData));
|
||||
|
||||
// Wait for the tagged response (tag + OK/NO)
|
||||
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
|
||||
qWarning() << "APPEND literal failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Seen flag
|
||||
QString seenFlag = read ? "+" : "-";
|
||||
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Seen failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Flagged flag
|
||||
QString flaggedFlag = flagged ? "+" : "-";
|
||||
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Flagged failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Update local DB
|
||||
auto items = MailItemDao::findByFolderId(fid);
|
||||
for (MailItem& item : items) {
|
||||
if (item.uid() == uid) {
|
||||
item.setRead(read);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as \\Deleted
|
||||
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Deleted failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// EXPUNGE (permanently delete)
|
||||
QString expungeCmd = "EXPUNGE";
|
||||
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "EXPUNGE failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Remove from local DB
|
||||
MailItemDao::remove(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ImapSynchronizer::generateEventId() const
|
||||
{
|
||||
return QString::number(QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
|
||||
for (const QString& line : lines) {
|
||||
QRegularExpressionMatch match = rx.match(line);
|
||||
if (match.hasMatch()) {
|
||||
QString delimiter = match.captured(1); // unused for now
|
||||
QString mailbox = match.captured(2);
|
||||
Folder folder;
|
||||
folder.setName(mailbox);
|
||||
QString lower = mailbox.toLower();
|
||||
if (lower == "inbox") folder.setInbox(true);
|
||||
else if (lower == "sent") folder.setSent(true);
|
||||
else if (lower == "drafts") folder.setDrafts(true);
|
||||
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
|
||||
folders.append(folder);
|
||||
}
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
int i = 0;
|
||||
while (i < lines.size()) {
|
||||
const QString& line = lines[i];
|
||||
if (!line.startsWith(QStringLiteral("* "))) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
|
||||
if (fetchPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract UID
|
||||
int uidPos = line.indexOf(QStringLiteral("UID "));
|
||||
if (uidPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
|
||||
if (uidEnd == -1) uidEnd = line.length();
|
||||
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
|
||||
bool ok;
|
||||
qint64 uid = uidStr.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
MailItem item;
|
||||
item.setUid(uid);
|
||||
|
||||
// Extract FLAGS
|
||||
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
|
||||
if (flagsPos != -1) {
|
||||
int flagsEnd = line.indexOf(')', flagsPos + 7);
|
||||
if (flagsEnd != -1) {
|
||||
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
||||
item.setRead(flags.contains(QStringLiteral("\\Seen")));
|
||||
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract INTERNALDATE
|
||||
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
|
||||
if (datePos != -1) {
|
||||
int dateStart = datePos + 15; // length of "INTERNALDATE \""
|
||||
int dateEnd = line.indexOf('\"', dateStart);
|
||||
if (dateEnd != -1) {
|
||||
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
|
||||
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
|
||||
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
|
||||
QString bodyData;
|
||||
int bodyPos = line.indexOf(QStringLiteral("BODY["));
|
||||
if (bodyPos != -1) {
|
||||
int bracePos = line.indexOf('{', bodyPos);
|
||||
if (bracePos != -1) {
|
||||
int sizeEnd = line.indexOf('}', bracePos);
|
||||
if (sizeEnd != -1) {
|
||||
bool sizeOk;
|
||||
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
|
||||
if (sizeOk) {
|
||||
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
|
||||
if (dataStart != -1) {
|
||||
dataStart += 2; // skip \r\n
|
||||
int available = line.length() - dataStart;
|
||||
if (available > size) available = size;
|
||||
bodyData = line.mid(dataStart, available);
|
||||
int received = available;
|
||||
int remaining = size - received;
|
||||
int j = i + 1;
|
||||
while (j < lines.size() && remaining > 0) {
|
||||
const QString& nextLine = lines[j];
|
||||
int take = qMin(nextLine.length(), remaining);
|
||||
bodyData += nextLine.left(take);
|
||||
remaining -= take;
|
||||
++j;
|
||||
}
|
||||
// we have consumed lines up to j-1
|
||||
i = j - 1; // will be incremented at end of loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse headers from bodyData
|
||||
if (!bodyData.isEmpty()) {
|
||||
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
|
||||
auto it = headerRx.globalMatch(bodyData);
|
||||
while (it.hasNext()) {
|
||||
auto match = it.next();
|
||||
QString key = match.captured(2).toLower().trimmed();
|
||||
QString value = match.captured(3).trimmed();
|
||||
if (key == QStringLiteral("subject"))
|
||||
item.setSubject(value);
|
||||
else if (key == QStringLiteral("from"))
|
||||
item.setSender(value);
|
||||
else if (key == QStringLiteral("to"))
|
||||
item.setRecipient(value);
|
||||
else if (key == QStringLiteral("date")) {
|
||||
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If subject not set, use placeholder
|
||||
if (item.subject().isEmpty())
|
||||
item.setSubject(QStringLiteral("(No Subject)"));
|
||||
|
||||
items.append(item);
|
||||
++i;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
#include "imapsynchronizer.h"
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include "../../core/mailitem.h"
|
||||
#include "../../core/models/account.h"
|
||||
#include "../../core/models/folder.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
|
||||
ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
||||
: Synchronizer(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::initialize(const Account& account)
|
||||
{
|
||||
const Account::ConnectionSettings& settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
m_useSsl = settings.incomingSsl;
|
||||
m_username = settings.username;
|
||||
m_password = settings.password;
|
||||
m_authMethod = settings.authMethod;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: connects and logs in to the IMAP server
|
||||
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
||||
{
|
||||
bool connected = false;
|
||||
bool loggedIn = false;
|
||||
QString response;
|
||||
|
||||
// Connect to host
|
||||
QEventLoop connectLoop;
|
||||
QTimer connectTimer;
|
||||
bool connectTimeout = false;
|
||||
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP connection error:" << err;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.setSingleShot(true);
|
||||
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
|
||||
connectTimeout = true;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.start(30000); // 30 seconds
|
||||
conn.connectToHost(m_host, m_port, m_useSsl);
|
||||
connectLoop.exec();
|
||||
if (connectTimeout) {
|
||||
qWarning() << "Connection timeout to" << m_host;
|
||||
return false;
|
||||
}
|
||||
connected = true;
|
||||
|
||||
// Wait for server greeting (first untagged response)
|
||||
QEventLoop greetLoop;
|
||||
bool gotGreeting = false;
|
||||
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
|
||||
[&](const QString& line) {
|
||||
Q_UNUSED(line);
|
||||
gotGreeting = true;
|
||||
greetLoop.quit();
|
||||
});
|
||||
QTimer greetTimer;
|
||||
greetTimer.setSingleShot(true);
|
||||
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
|
||||
greetLoop.quit();
|
||||
});
|
||||
greetTimer.start(30000); // 30 seconds
|
||||
greetLoop.exec();
|
||||
QObject::disconnect(greetConn);
|
||||
if (!gotGreeting) {
|
||||
qWarning() << "Timeout waiting for server greeting";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Login
|
||||
QEventLoop loginLoop;
|
||||
QTimer loginTimer;
|
||||
bool loginTimeout = false;
|
||||
bool loginSuccess = false;
|
||||
// Already connected, now login
|
||||
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
|
||||
if (ok) {
|
||||
loginSuccess = true;
|
||||
} else {
|
||||
qWarning() << "Login failed:" << resp;
|
||||
}
|
||||
loginLoop.quit();
|
||||
});
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP error during login:" << err;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.setSingleShot(true);
|
||||
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
|
||||
loginTimeout = true;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.start(30000);
|
||||
loginLoop.exec();
|
||||
if (loginTimeout) {
|
||||
qWarning() << "Login timeout";
|
||||
return false;
|
||||
}
|
||||
if (!loginSuccess) {
|
||||
return false;
|
||||
}
|
||||
loggedIn = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::syncFolder(const Folder& folder)
|
||||
{
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// Select folder
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
|
||||
QString selectResp;
|
||||
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed for" << folder.name();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all UIDs
|
||||
QString searchCmd = "UID SEARCH ALL";
|
||||
QString searchResp;
|
||||
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
|
||||
qWarning() << "SEARCH failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResp.split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no messages, set unread count to 0 and exit
|
||||
if (uids.isEmpty()) {
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(0);
|
||||
FolderDao::update(f);
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get flags for all UIDs in one command
|
||||
QStringList uidStrs;
|
||||
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
|
||||
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
|
||||
QString fetchResp;
|
||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
|
||||
qWarning() << "FETCH FLAGS failed";
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse flags and update local DB
|
||||
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
|
||||
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
|
||||
auto it = flagsRx.globalMatch(fetchResp);
|
||||
while (it.hasNext()) {
|
||||
auto m = it.next();
|
||||
qint64 uid = m.captured(1).toLongLong();
|
||||
QString flags = m.captured(2);
|
||||
bool seen = flags.contains("\\Seen");
|
||||
bool flagged = flags.contains("\\Flagged");
|
||||
flagsMap[uid] = qMakePair(seen, flagged);
|
||||
}
|
||||
|
||||
// Update local items
|
||||
auto localItems = MailItemDao::findByFolderId(folder.id());
|
||||
for (MailItem& item : localItems) {
|
||||
if (flagsMap.contains(item.uid())) {
|
||||
bool seen = flagsMap[item.uid()].first;
|
||||
bool flagged = flagsMap[item.uid()].second;
|
||||
if (item.isRead() != seen || item.isFlagged() != flagged) {
|
||||
item.setRead(seen);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count unread
|
||||
int unread = 0;
|
||||
for (const MailItem& item : localItems) {
|
||||
if (!item.isRead()) unread++;
|
||||
}
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(unread);
|
||||
FolderDao::update(f);
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Get folder name from DB
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return items;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// SELECT with real folder name
|
||||
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString selectResponse;
|
||||
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
|
||||
qWarning() << "SELECT command failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
if (!selectResponse.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// SEARCH
|
||||
QString searchCommand;
|
||||
if (sinceUid > 0) {
|
||||
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
|
||||
} else {
|
||||
searchCommand = "UID SEARCH ALL";
|
||||
}
|
||||
QString searchResponse;
|
||||
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
|
||||
qWarning() << "SEARCH command failed:" << searchResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// Parse UIDs (robust: look for line starting with "* SEARCH")
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResponse.split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uids.isEmpty()) {
|
||||
conn.sendCommandWait("CLOSE", searchResponse, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
// Fetch in batches of 50 UIDs
|
||||
const int batchSize = 50;
|
||||
for (int i = 0; i < uids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = uids.mid(i, qMin(batchSize, uids.size() - i));
|
||||
QString batchList;
|
||||
for (qint64 uid : batch) {
|
||||
batchList.append(QString::number(uid)).append(",");
|
||||
}
|
||||
batchList.chop(1); // remove trailing comma
|
||||
|
||||
// Fetch headers and flags (efficient)
|
||||
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
|
||||
.arg(batchList);
|
||||
QString fetchResponse;
|
||||
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
|
||||
qWarning() << "FETCH failed for batch" << i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse and persist
|
||||
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
|
||||
for (MailItem& item : batchItems) {
|
||||
item.setFolderId(fid);
|
||||
if (MailItemDao::insert(item)) {
|
||||
items.append(item);
|
||||
} else {
|
||||
qWarning() << "Failed to insert mail item uid" << item.uid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close and disconnect
|
||||
QString dummy;
|
||||
conn.sendCommandWait("CLOSE", dummy, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::getFolders() const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return folders;
|
||||
}
|
||||
|
||||
// List folders: LIST "" "*"
|
||||
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(listCommand, response, 30000)) {
|
||||
qWarning() << "LIST command failed:" << response;
|
||||
return folders;
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
QStringList lines = response.split('\n');
|
||||
folders = parseListResponse(lines);
|
||||
|
||||
// Logout
|
||||
conn.disconnect();
|
||||
return folders;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||
{
|
||||
// Get folder name
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return false;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// Build RFC822 message (headers + body)
|
||||
QStringList headers;
|
||||
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
|
||||
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
|
||||
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
|
||||
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
|
||||
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
|
||||
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
|
||||
headers << "MIME-Version: 1.0";
|
||||
headers << "Content-Type: text/html; charset=UTF-8";
|
||||
headers << "Content-Transfer-Encoding: 7bit";
|
||||
headers << ""; // blank line separates headers from body
|
||||
headers << item.bodyHtml(); // using bodyHtml as the body
|
||||
|
||||
QString message = headers.join("\r\n");
|
||||
QByteArray msgData = message.toUtf8();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT folder (some servers require it)
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
|
||||
|
||||
// APPEND with literal: APPEND "folder" (\Seen) {size}
|
||||
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
|
||||
qWarning() << "APPEND command initial failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the literal (message data)
|
||||
conn.sendRaw(QString::fromUtf8(msgData));
|
||||
|
||||
// Wait for the tagged response (tag + OK/NO)
|
||||
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
|
||||
qWarning() << "APPEND literal failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Seen flag
|
||||
QString seenFlag = read ? "+" : "-";
|
||||
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Seen failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Flagged flag
|
||||
QString flaggedFlag = flagged ? "+" : "-";
|
||||
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Flagged failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Update local DB
|
||||
auto items = MailItemDao::findByFolderId(fid);
|
||||
for (MailItem& item : items) {
|
||||
if (item.uid() == uid) {
|
||||
item.setRead(read);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as \\Deleted
|
||||
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Deleted failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// EXPUNGE (permanently delete)
|
||||
QString expungeCmd = "EXPUNGE";
|
||||
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "EXPUNGE failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Remove from local DB
|
||||
MailItemDao::remove(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ImapSynchronizer::generateEventId() const
|
||||
{
|
||||
return QString::number(QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
|
||||
for (const QString& line : lines) {
|
||||
QRegularExpressionMatch match = rx.match(line);
|
||||
if (match.hasMatch()) {
|
||||
QString delimiter = match.captured(1); // unused for now
|
||||
QString mailbox = match.captured(2);
|
||||
Folder folder;
|
||||
folder.setName(mailbox);
|
||||
QString lower = mailbox.toLower();
|
||||
if (lower == "inbox") folder.setInbox(true);
|
||||
else if (lower == "sent") folder.setSent(true);
|
||||
else if (lower == "drafts") folder.setDrafts(true);
|
||||
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
|
||||
folders.append(folder);
|
||||
}
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
int i = 0;
|
||||
while (i < lines.size()) {
|
||||
const QString& line = lines[i];
|
||||
if (!line.startsWith(QStringLiteral("* "))) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
|
||||
if (fetchPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract UID
|
||||
int uidPos = line.indexOf(QStringLiteral("UID "));
|
||||
if (uidPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
|
||||
if (uidEnd == -1) uidEnd = line.length();
|
||||
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
|
||||
bool ok;
|
||||
qint64 uid = uidStr.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
MailItem item;
|
||||
item.setUid(uid);
|
||||
|
||||
// Extract FLAGS
|
||||
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
|
||||
if (flagsPos != -1) {
|
||||
int flagsEnd = line.indexOf(')', flagsPos + 7);
|
||||
if (flagsEnd != -1) {
|
||||
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
||||
item.setRead(flags.contains(QStringLiteral("\\Seen")));
|
||||
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract INTERNALDATE
|
||||
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
|
||||
if (datePos != -1) {
|
||||
int dateStart = datePos + 15; // length of "INTERNALDATE \""
|
||||
int dateEnd = line.indexOf('\"', dateStart);
|
||||
if (dateEnd != -1) {
|
||||
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
|
||||
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
|
||||
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
|
||||
QString bodyData;
|
||||
int bodyPos = line.indexOf(QStringLiteral("BODY["));
|
||||
if (bodyPos != -1) {
|
||||
int bracePos = line.indexOf('{', bodyPos);
|
||||
if (bracePos != -1) {
|
||||
int sizeEnd = line.indexOf('}', bracePos);
|
||||
if (sizeEnd != -1) {
|
||||
bool sizeOk;
|
||||
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
|
||||
if (sizeOk) {
|
||||
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
|
||||
if (dataStart != -1) {
|
||||
dataStart += 2; // skip \r\n
|
||||
int available = line.length() - dataStart;
|
||||
if (available > size) available = size;
|
||||
bodyData = line.mid(dataStart, available);
|
||||
int received = available;
|
||||
int remaining = size - received;
|
||||
int j = i + 1;
|
||||
while (j < lines.size() && remaining > 0) {
|
||||
const QString& nextLine = lines[j];
|
||||
int take = qMin(nextLine.length(), remaining);
|
||||
bodyData += nextLine.left(take);
|
||||
remaining -= take;
|
||||
++j;
|
||||
}
|
||||
// we have consumed lines up to j-1
|
||||
i = j - 1; // will be incremented at end of loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse headers from bodyData
|
||||
if (!bodyData.isEmpty()) {
|
||||
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
|
||||
auto it = headerRx.globalMatch(bodyData);
|
||||
while (it.hasNext()) {
|
||||
auto match = it.next();
|
||||
QString key = match.captured(2).toLower().trimmed();
|
||||
QString value = match.captured(3).trimmed();
|
||||
if (key == QStringLiteral("subject"))
|
||||
item.setSubject(value);
|
||||
else if (key == QStringLiteral("from"))
|
||||
item.setSender(value);
|
||||
else if (key == QStringLiteral("to"))
|
||||
item.setRecipient(value);
|
||||
else if (key == QStringLiteral("date")) {
|
||||
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If subject not set, use placeholder
|
||||
if (item.subject().isEmpty())
|
||||
item.setSubject(QStringLiteral("(No Subject)"));
|
||||
|
||||
items.append(item);
|
||||
++i;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
#include "imapsynchronizer.h"
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include "../../core/mailitem.h"
|
||||
#include "../../core/models/account.h"
|
||||
#include "../../core/models/folder.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
#include <algorithm>
|
||||
|
||||
ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
||||
: Synchronizer(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::initialize(const Account& account)
|
||||
{
|
||||
const Account::ConnectionSettings& settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
m_useSsl = settings.incomingSsl;
|
||||
m_username = settings.username;
|
||||
m_password = settings.password;
|
||||
m_authMethod = settings.authMethod;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: connects and logs in to the IMAP server
|
||||
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
||||
{
|
||||
bool connected = false;
|
||||
bool loggedIn = false;
|
||||
QString response;
|
||||
|
||||
// Connect to host
|
||||
QEventLoop connectLoop;
|
||||
QTimer connectTimer;
|
||||
bool connectTimeout = false;
|
||||
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP connection error:" << err;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.setSingleShot(true);
|
||||
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
|
||||
connectTimeout = true;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.start(30000); // 30 seconds
|
||||
conn.connectToHost(m_host, m_port, m_useSsl);
|
||||
connectLoop.exec();
|
||||
if (connectTimeout) {
|
||||
qWarning() << "Connection timeout to" << m_host;
|
||||
return false;
|
||||
}
|
||||
connected = true;
|
||||
|
||||
// Wait for server greeting (first untagged response)
|
||||
QEventLoop greetLoop;
|
||||
bool gotGreeting = false;
|
||||
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
|
||||
[&](const QString& line) {
|
||||
Q_UNUSED(line);
|
||||
gotGreeting = true;
|
||||
greetLoop.quit();
|
||||
});
|
||||
QTimer greetTimer;
|
||||
greetTimer.setSingleShot(true);
|
||||
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
|
||||
greetLoop.quit();
|
||||
});
|
||||
greetTimer.start(30000); // 30 seconds
|
||||
greetLoop.exec();
|
||||
QObject::disconnect(greetConn);
|
||||
if (!gotGreeting) {
|
||||
qWarning() << "Timeout waiting for server greeting";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Login
|
||||
QEventLoop loginLoop;
|
||||
QTimer loginTimer;
|
||||
bool loginTimeout = false;
|
||||
bool loginSuccess = false;
|
||||
// Already connected, now login
|
||||
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
|
||||
if (ok) {
|
||||
loginSuccess = true;
|
||||
} else {
|
||||
qWarning() << "Login failed:" << resp;
|
||||
}
|
||||
loginLoop.quit();
|
||||
});
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP error during login:" << err;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.setSingleShot(true);
|
||||
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
|
||||
loginTimeout = true;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.start(30000);
|
||||
loginLoop.exec();
|
||||
if (loginTimeout) {
|
||||
qWarning() << "Login timeout";
|
||||
return false;
|
||||
}
|
||||
if (!loginSuccess) {
|
||||
return false;
|
||||
}
|
||||
loggedIn = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<qint64> ImapSynchronizer::parseUidSearchResponse(const QString& response) const
|
||||
{
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = response.split(QStringLiteral("\n"));
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith(QStringLiteral("* SEARCH"))) {
|
||||
QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0)
|
||||
uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return uids;
|
||||
}
|
||||
bool ImapSynchronizer::syncFolder(const Folder& folder)
|
||||
{
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn))
|
||||
return false;
|
||||
|
||||
// Select folder
|
||||
QString selectCmd = QStringLiteral("SELECT \"%1\"").arg(folder.name());
|
||||
QString selectResp;
|
||||
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(QStringLiteral(" OK "))) {
|
||||
qWarning() << "SELECT failed for" << folder.name();
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all UIDs from server
|
||||
QString searchAllCmd = QStringLiteral("UID SEARCH ALL");
|
||||
QString searchAllResp;
|
||||
if (!conn.sendCommandWait(searchAllCmd, searchAllResp, 30000) || !searchAllResp.contains(QStringLiteral(" OK "))) {
|
||||
qWarning() << "UID SEARCH ALL failed";
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
QVector<qint64> serverUids = parseUidSearchResponse(searchAllResp);
|
||||
std::sort(serverUids.begin(), serverUids.end());
|
||||
|
||||
// Get local UIDs for this folder
|
||||
int fid = folder.id();
|
||||
QVector<qint64> localUids = MailItemDao::getUidsForFolder(fid);
|
||||
std::sort(localUids.begin(), localUids.end());
|
||||
|
||||
// Determine last known UID (max local uid)
|
||||
qint64 lastUid = 0;
|
||||
if (!localUids.isEmpty())
|
||||
lastUid = localUids.last();
|
||||
|
||||
// Fetch new UIDs (those > lastUid)
|
||||
QVector<qint64> newUids;
|
||||
if (lastUid == 0) {
|
||||
// No local mails, treat all as new
|
||||
newUids = serverUids;
|
||||
} else {
|
||||
for (qint64 uid : serverUids) {
|
||||
if (uid > lastUid)
|
||||
newUids.append(uid);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new messages in batches
|
||||
if (!newUids.isEmpty()) {
|
||||
const int batchSize = 50;
|
||||
for (int i = 0; i < newUids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = newUids.mid(i, qMin(batchSize, newUids.size() - i));
|
||||
QStringList uidStrs;
|
||||
for (qint64 uid : batch)
|
||||
uidStrs.append(QString::number(uid));
|
||||
QString batchList = uidStrs.join(QStringLiteral(","));
|
||||
QString fetchCmd = QStringLiteral("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)").arg(batchList);
|
||||
QString fetchResp;
|
||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) {
|
||||
qWarning() << "UID FETCH failed for batch" << i;
|
||||
continue;
|
||||
}
|
||||
if (!fetchResp.contains(QStringLiteral(" OK "))) {
|
||||
qWarning() << "UID FETCH did not return OK";
|
||||
continue;
|
||||
}
|
||||
QVector<MailItem> fetched = parseFetchResponse(fetchResp.split(QStringLiteral("\n")));
|
||||
for (MailItem& item : fetched) {
|
||||
item.setFolderId(fid);
|
||||
if (!MailItemDao::insert(item)) {
|
||||
qWarning() << "Failed to insert mail item uid" << item.uid();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update flags for all local uids (to capture flag changes)
|
||||
if (!localUids.isEmpty()) {
|
||||
const int batchSize = 100;
|
||||
for (int i = 0; i < localUids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = localUids.mid(i, qMin(batchSize, localUids.size() - i));
|
||||
QStringList uidStrs;
|
||||
for (qint64 uid : batch)
|
||||
uidStrs.append(QString::number(uid));
|
||||
QString uidList = uidStrs.join(QStringLiteral(","));
|
||||
QString fetchCmd = QStringLiteral("UID FETCH %1 (FLAGS)").arg(uidList);
|
||||
QString fetchResp;
|
||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) {
|
||||
qWarning() << "UID FETCH FLAGS failed";
|
||||
continue;
|
||||
}
|
||||
if (!fetchResp.contains(QStringLiteral(" OK "))) {
|
||||
qWarning() << "UID FETCH FLAGS not OK";
|
||||
continue;
|
||||
}
|
||||
// Parse flags response
|
||||
QRegularExpression flagsRx(QStringLiteral(R"(UID (\d+).*FLAGS \(([^)]*)\))"));
|
||||
auto it = flagsRx.globalMatch(fetchResp);
|
||||
while (it.hasNext()) {
|
||||
auto m = it.next();
|
||||
qint64 uid = m.captured(1).toLongLong();
|
||||
QString flags = m.captured(2);
|
||||
bool seen = flags.contains(QStringLiteral("\\Seen"));
|
||||
bool flagged = flags.contains(QStringLiteral("\\Flagged"));
|
||||
std::optional<MailItem> opt = MailItemDao::findById(uid);
|
||||
if (opt) {
|
||||
MailItem& item = *opt;
|
||||
bool changed = false;
|
||||
if (item.isRead() != seen) {
|
||||
item.setRead(seen);
|
||||
changed = true;
|
||||
}
|
||||
if (item.isFlagged() != flagged) {
|
||||
item.setFlagged(flagged);
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
MailItemDao::update(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle expunged messages: remove local uids not present on server
|
||||
QVector<qint64> toRemove;
|
||||
std::set_difference(localUids.begin(), localUids.end(),
|
||||
serverUids.begin(), serverUids.end(),
|
||||
std::back_inserter(toRemove));
|
||||
for (qint64 uid : toRemove) {
|
||||
MailItemDao::remove(uid);
|
||||
}
|
||||
|
||||
// Update unread count
|
||||
int unread = 0;
|
||||
for (qint64 uid : localUids) {
|
||||
std::optional<MailItem> opt = MailItemDao::findById(uid);
|
||||
if (opt && !opt->isRead())
|
||||
++unread;
|
||||
}
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(unread);
|
||||
if (!FolderDao::update(f)) {
|
||||
qWarning() << "Failed to update folder unread count";
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Get folder name from DB
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return items;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// SELECT with real folder name
|
||||
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString selectResponse;
|
||||
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
|
||||
qWarning() << "SELECT command failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
if (!selectResponse.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// SEARCH
|
||||
QString searchCommand;
|
||||
if (sinceUid > 0) {
|
||||
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
|
||||
} else {
|
||||
searchCommand = "UID SEARCH ALL";
|
||||
}
|
||||
QString searchResponse;
|
||||
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
|
||||
qWarning() << "SEARCH command failed:" << searchResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// Parse UIDs (robust: look for line starting with "* SEARCH")
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResponse.split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uids.isEmpty()) {
|
||||
conn.sendCommandWait("CLOSE", searchResponse, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
// Fetch in batches of 50 UIDs
|
||||
const int batchSize = 50;
|
||||
for (int i = 0; i < uids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = uids.mid(i, qMin(batchSize, uids.size() - i));
|
||||
QString batchList;
|
||||
for (qint64 uid : batch) {
|
||||
batchList.append(QString::number(uid)).append(",");
|
||||
}
|
||||
batchList.chop(1); // remove trailing comma
|
||||
|
||||
// Fetch headers and flags (efficient)
|
||||
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
|
||||
.arg(batchList);
|
||||
QString fetchResponse;
|
||||
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
|
||||
qWarning() << "FETCH failed for batch" << i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse and persist
|
||||
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
|
||||
for (MailItem& item : batchItems) {
|
||||
item.setFolderId(fid);
|
||||
if (MailItemDao::insert(item)) {
|
||||
items.append(item);
|
||||
} else {
|
||||
qWarning() << "Failed to insert mail item uid" << item.uid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close and disconnect
|
||||
QString dummy;
|
||||
conn.sendCommandWait("CLOSE", dummy, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::getFolders() const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return folders;
|
||||
}
|
||||
|
||||
// List folders: LIST "" "*"
|
||||
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(listCommand, response, 30000)) {
|
||||
qWarning() << "LIST command failed:" << response;
|
||||
return folders;
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
QStringList lines = response.split('\n');
|
||||
folders = parseListResponse(lines);
|
||||
|
||||
// Logout
|
||||
conn.disconnect();
|
||||
return folders;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||
{
|
||||
// Get folder name
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return false;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// Build RFC822 message (headers + body)
|
||||
QStringList headers;
|
||||
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
|
||||
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
|
||||
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
|
||||
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
|
||||
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
|
||||
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
|
||||
headers << "MIME-Version: 1.0";
|
||||
headers << "Content-Type: text/html; charset=UTF-8";
|
||||
headers << "Content-Transfer-Encoding: 7bit";
|
||||
headers << ""; // blank line separates headers from body
|
||||
headers << item.bodyHtml(); // using bodyHtml as the body
|
||||
|
||||
QString message = headers.join("\r\n");
|
||||
QByteArray msgData = message.toUtf8();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT folder (some servers require it)
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
|
||||
|
||||
// APPEND with literal: APPEND "folder" (\Seen) {size}
|
||||
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
|
||||
qWarning() << "APPEND command initial failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the literal (message data)
|
||||
conn.sendRaw(QString::fromUtf8(msgData));
|
||||
|
||||
// Wait for the tagged response (tag + OK/NO)
|
||||
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
|
||||
qWarning() << "APPEND literal failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Seen flag
|
||||
QString seenFlag = read ? "+" : "-";
|
||||
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Seen failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Flagged flag
|
||||
QString flaggedFlag = flagged ? "+" : "-";
|
||||
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Flagged failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Update local DB
|
||||
auto items = MailItemDao::findByFolderId(fid);
|
||||
for (MailItem& item : items) {
|
||||
if (item.uid() == uid) {
|
||||
item.setRead(read);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as \\Deleted
|
||||
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Deleted failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// EXPUNGE (permanently delete)
|
||||
QString expungeCmd = "EXPUNGE";
|
||||
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "EXPUNGE failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Remove from local DB
|
||||
MailItemDao::remove(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ImapSynchronizer::generateEventId() const
|
||||
{
|
||||
return QString::number(QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
|
||||
for (const QString& line : lines) {
|
||||
QRegularExpressionMatch match = rx.match(line);
|
||||
if (match.hasMatch()) {
|
||||
QString delimiter = match.captured(1); // unused for now
|
||||
QString mailbox = match.captured(2);
|
||||
Folder folder;
|
||||
folder.setName(mailbox);
|
||||
QString lower = mailbox.toLower();
|
||||
if (lower == "inbox") folder.setInbox(true);
|
||||
else if (lower == "sent") folder.setSent(true);
|
||||
else if (lower == "drafts") folder.setDrafts(true);
|
||||
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
|
||||
folders.append(folder);
|
||||
}
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
int i = 0;
|
||||
while (i < lines.size()) {
|
||||
const QString& line = lines[i];
|
||||
if (!line.startsWith(QStringLiteral("* "))) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
|
||||
if (fetchPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract UID
|
||||
int uidPos = line.indexOf(QStringLiteral("UID "));
|
||||
if (uidPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
|
||||
if (uidEnd == -1) uidEnd = line.length();
|
||||
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
|
||||
bool ok;
|
||||
qint64 uid = uidStr.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
MailItem item;
|
||||
item.setUid(uid);
|
||||
|
||||
// Extract FLAGS
|
||||
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
|
||||
if (flagsPos != -1) {
|
||||
int flagsEnd = line.indexOf(')', flagsPos + 7);
|
||||
if (flagsEnd != -1) {
|
||||
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
||||
item.setRead(flags.contains(QStringLiteral("\\Seen")));
|
||||
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract INTERNALDATE
|
||||
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
|
||||
if (datePos != -1) {
|
||||
int dateStart = datePos + 15; // length of "INTERNALDATE \""
|
||||
int dateEnd = line.indexOf('\"', dateStart);
|
||||
if (dateEnd != -1) {
|
||||
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
|
||||
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
|
||||
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
|
||||
QString bodyData;
|
||||
int bodyPos = line.indexOf(QStringLiteral("BODY["));
|
||||
if (bodyPos != -1) {
|
||||
int bracePos = line.indexOf('{', bodyPos);
|
||||
if (bracePos != -1) {
|
||||
int sizeEnd = line.indexOf('}', bracePos);
|
||||
if (sizeEnd != -1) {
|
||||
bool sizeOk;
|
||||
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
|
||||
if (sizeOk) {
|
||||
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
|
||||
if (dataStart != -1) {
|
||||
dataStart += 2; // skip \r\n
|
||||
int available = line.length() - dataStart;
|
||||
if (available > size) available = size;
|
||||
bodyData = line.mid(dataStart, available);
|
||||
int received = available;
|
||||
int remaining = size - received;
|
||||
int j = i + 1;
|
||||
while (j < lines.size() && remaining > 0) {
|
||||
const QString& nextLine = lines[j];
|
||||
int take = qMin(nextLine.length(), remaining);
|
||||
bodyData += nextLine.left(take);
|
||||
remaining -= take;
|
||||
++j;
|
||||
}
|
||||
// we have consumed lines up to j-1
|
||||
i = j - 1; // will be incremented at end of loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse headers from bodyData
|
||||
if (!bodyData.isEmpty()) {
|
||||
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
|
||||
auto it = headerRx.globalMatch(bodyData);
|
||||
while (it.hasNext()) {
|
||||
auto match = it.next();
|
||||
QString key = match.captured(2).toLower().trimmed();
|
||||
QString value = match.captured(3).trimmed();
|
||||
if (key == QStringLiteral("subject"))
|
||||
item.setSubject(value);
|
||||
else if (key == QStringLiteral("from"))
|
||||
item.setSender(value);
|
||||
else if (key == QStringLiteral("to"))
|
||||
item.setRecipient(value);
|
||||
else if (key == QStringLiteral("date")) {
|
||||
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If subject not set, use placeholder
|
||||
if (item.subject().isEmpty())
|
||||
item.setSubject(QStringLiteral("(No Subject)"));
|
||||
|
||||
items.append(item);
|
||||
++i;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
#include "imapsynchronizer.h"
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include "../../core/mailitem.h"
|
||||
#include "../../core/models/account.h"
|
||||
#include "../../core/models/folder.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
#include <algorithm>
|
||||
|
||||
ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
||||
: Synchronizer(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::initialize(const Account& account)
|
||||
{
|
||||
const Account::ConnectionSettings& settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
m_useSsl = settings.incomingSsl;
|
||||
m_username = settings.username;
|
||||
m_password = settings.password;
|
||||
m_authMethod = settings.authMethod;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: connects and logs in to the IMAP server
|
||||
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
||||
{
|
||||
bool connected = false;
|
||||
bool loggedIn = false;
|
||||
QString response;
|
||||
|
||||
// Connect to host
|
||||
QEventLoop connectLoop;
|
||||
QTimer connectTimer;
|
||||
bool connectTimeout = false;
|
||||
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP connection error:" << err;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.setSingleShot(true);
|
||||
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
|
||||
connectTimeout = true;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.start(30000); // 30 seconds
|
||||
conn.connectToHost(m_host, m_port, m_useSsl);
|
||||
connectLoop.exec();
|
||||
if (connectTimeout) {
|
||||
qWarning() << "Connection timeout to" << m_host;
|
||||
return false;
|
||||
}
|
||||
connected = true;
|
||||
|
||||
// Wait for server greeting (first untagged response)
|
||||
QEventLoop greetLoop;
|
||||
bool gotGreeting = false;
|
||||
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
|
||||
[&](const QString& line) {
|
||||
Q_UNUSED(line);
|
||||
gotGreeting = true;
|
||||
greetLoop.quit();
|
||||
});
|
||||
QTimer greetTimer;
|
||||
greetTimer.setSingleShot(true);
|
||||
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
|
||||
greetLoop.quit();
|
||||
});
|
||||
greetTimer.start(30000); // 30 seconds
|
||||
greetLoop.exec();
|
||||
QObject::disconnect(greetConn);
|
||||
if (!gotGreeting) {
|
||||
qWarning() << "Timeout waiting for server greeting";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Login
|
||||
QEventLoop loginLoop;
|
||||
QTimer loginTimer;
|
||||
bool loginTimeout = false;
|
||||
bool loginSuccess = false;
|
||||
// Already connected, now login
|
||||
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
|
||||
if (ok) {
|
||||
loginSuccess = true;
|
||||
} else {
|
||||
qWarning() << "Login failed:" << resp;
|
||||
}
|
||||
loginLoop.quit();
|
||||
});
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP error during login:" << err;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.setSingleShot(true);
|
||||
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
|
||||
loginTimeout = true;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.start(30000);
|
||||
loginLoop.exec();
|
||||
if (loginTimeout) {
|
||||
qWarning() << "Login timeout";
|
||||
return false;
|
||||
}
|
||||
if (!loginSuccess) {
|
||||
return false;
|
||||
}
|
||||
loggedIn = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<qint64> ImapSynchronizer::parseUidSearchResponse(const QString& response) const
|
||||
{
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = response.split(QStringLiteral("\n"));
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith(QStringLiteral("* SEARCH"))) {
|
||||
QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0)
|
||||
uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return uids;
|
||||
}
|
||||
bool ImapSynchronizer::syncFolder(const Folder& folder)
|
||||
{
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn))
|
||||
return false;
|
||||
|
||||
// Select folder
|
||||
QString selectCmd = QStringLiteral("SELECT \"%1\"").arg(folder.name());
|
||||
QString selectResp;
|
||||
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(QStringLiteral(" OK "))) {
|
||||
qWarning() << "SELECT failed for" << folder.name();
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all UIDs from server
|
||||
QString searchAllCmd = QStringLiteral("UID SEARCH ALL");
|
||||
QString searchAllResp;
|
||||
if (!conn.sendCommandWait(searchAllCmd, searchAllResp, 30000) || !searchAllResp.contains(QStringLiteral(" OK "))) {
|
||||
qWarning() << "UID SEARCH ALL failed";
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
QVector<qint64> serverUids = parseUidSearchResponse(searchAllResp);
|
||||
std::sort(serverUids.begin(), serverUids.end());
|
||||
|
||||
// Get local UIDs for this folder
|
||||
int fid = folder.id();
|
||||
QVector<qint64> localUids = MailItemDao::getUidsForFolder(fid);
|
||||
std::sort(localUids.begin(), localUids.end());
|
||||
|
||||
// Determine last known UID (max local uid)
|
||||
qint64 lastUid = 0;
|
||||
if (!localUids.isEmpty())
|
||||
lastUid = localUids.last();
|
||||
|
||||
// Fetch new UIDs (those > lastUid)
|
||||
QVector<qint64> newUids;
|
||||
if (lastUid == 0) {
|
||||
// No local mails, treat all as new
|
||||
newUids = serverUids;
|
||||
} else {
|
||||
for (qint64 uid : serverUids) {
|
||||
if (uid > lastUid)
|
||||
newUids.append(uid);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch new messages in batches
|
||||
if (!newUids.isEmpty()) {
|
||||
const int batchSize = 50;
|
||||
for (int i = 0; i < newUids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = newUids.mid(i, qMin(batchSize, newUids.size() - i));
|
||||
QStringList uidStrs;
|
||||
for (qint64 uid : batch)
|
||||
uidStrs.append(QString::number(uid));
|
||||
QString batchList = uidStrs.join(QStringLiteral(","));
|
||||
QString fetchCmd = QStringLiteral("UID FETCH %1 (BODY[] FLAGS INTERNALDATE)").arg(batchList);
|
||||
QString fetchResp;
|
||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) {
|
||||
qWarning() << "UID FETCH failed for batch" << i;
|
||||
continue;
|
||||
}
|
||||
if (!fetchResp.contains(QStringLiteral(" OK "))) {
|
||||
qWarning() << "UID FETCH did not return OK";
|
||||
continue;
|
||||
}
|
||||
QVector<MailItem> fetched = parseFetchResponse(fetchResp.split(QStringLiteral("\n")));
|
||||
for (MailItem& item : fetched) {
|
||||
item.setFolderId(fid);
|
||||
if (!MailItemDao::insert(item)) {
|
||||
qWarning() << "Failed to insert mail item uid" << item.uid();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update flags for all local uids (to capture flag changes)
|
||||
if (!localUids.isEmpty()) {
|
||||
const int batchSize = 100;
|
||||
for (int i = 0; i < localUids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = localUids.mid(i, qMin(batchSize, localUids.size() - i));
|
||||
QStringList uidStrs;
|
||||
for (qint64 uid : batch)
|
||||
uidStrs.append(QString::number(uid));
|
||||
QString uidList = uidStrs.join(QStringLiteral(","));
|
||||
QString fetchCmd = QStringLiteral("UID FETCH %1 (FLAGS)").arg(uidList);
|
||||
QString fetchResp;
|
||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) {
|
||||
qWarning() << "UID FETCH FLAGS failed";
|
||||
continue;
|
||||
}
|
||||
if (!fetchResp.contains(QStringLiteral(" OK "))) {
|
||||
qWarning() << "UID FETCH FLAGS not OK";
|
||||
continue;
|
||||
}
|
||||
// Parse flags response
|
||||
QRegularExpression flagsRx(QStringLiteral(R"(UID (\d+).*FLAGS \(([^)]*)\))"));
|
||||
auto it = flagsRx.globalMatch(fetchResp);
|
||||
while (it.hasNext()) {
|
||||
auto m = it.next();
|
||||
qint64 uid = m.captured(1).toLongLong();
|
||||
QString flags = m.captured(2);
|
||||
bool seen = flags.contains(QStringLiteral("\\Seen"));
|
||||
bool flagged = flags.contains(QStringLiteral("\\Flagged"));
|
||||
std::optional<MailItem> opt = MailItemDao::findById(uid);
|
||||
if (opt) {
|
||||
MailItem& item = *opt;
|
||||
bool changed = false;
|
||||
if (item.isRead() != seen) {
|
||||
item.setRead(seen);
|
||||
changed = true;
|
||||
}
|
||||
if (item.isFlagged() != flagged) {
|
||||
item.setFlagged(flagged);
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
MailItemDao::update(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle expunged messages: remove local uids not present on server
|
||||
QVector<qint64> toRemove;
|
||||
std::set_difference(localUids.begin(), localUids.end(),
|
||||
serverUids.begin(), serverUids.end(),
|
||||
std::back_inserter(toRemove));
|
||||
for (qint64 uid : toRemove) {
|
||||
MailItemDao::remove(uid);
|
||||
}
|
||||
|
||||
// Update unread count
|
||||
int unread = 0;
|
||||
for (qint64 uid : localUids) {
|
||||
std::optional<MailItem> opt = MailItemDao::findById(uid);
|
||||
if (opt && !opt->isRead())
|
||||
++unread;
|
||||
}
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(unread);
|
||||
if (!FolderDao::update(f)) {
|
||||
qWarning() << "Failed to update folder unread count";
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Get folder name from DB
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return items;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// SELECT with real folder name
|
||||
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString selectResponse;
|
||||
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
|
||||
qWarning() << "SELECT command failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
if (!selectResponse.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// SEARCH
|
||||
QString searchCommand;
|
||||
if (sinceUid > 0) {
|
||||
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
|
||||
} else {
|
||||
searchCommand = "UID SEARCH ALL";
|
||||
}
|
||||
QString searchResponse;
|
||||
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
|
||||
qWarning() << "SEARCH command failed:" << searchResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// Parse UIDs (robust: look for line starting with "* SEARCH")
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResponse.split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uids.isEmpty()) {
|
||||
conn.sendCommandWait("CLOSE", searchResponse, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
// Fetch in batches of 50 UIDs
|
||||
const int batchSize = 50;
|
||||
for (int i = 0; i < uids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = uids.mid(i, qMin(batchSize, uids.size() - i));
|
||||
QString batchList;
|
||||
for (qint64 uid : batch) {
|
||||
batchList.append(QString::number(uid)).append(",");
|
||||
}
|
||||
batchList.chop(1); // remove trailing comma
|
||||
|
||||
// Fetch headers and flags (efficient)
|
||||
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
|
||||
.arg(batchList);
|
||||
QString fetchResponse;
|
||||
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
|
||||
qWarning() << "FETCH failed for batch" << i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse and persist
|
||||
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
|
||||
for (MailItem& item : batchItems) {
|
||||
item.setFolderId(fid);
|
||||
if (MailItemDao::insert(item)) {
|
||||
items.append(item);
|
||||
} else {
|
||||
qWarning() << "Failed to insert mail item uid" << item.uid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close and disconnect
|
||||
QString dummy;
|
||||
conn.sendCommandWait("CLOSE", dummy, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::getFolders() const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return folders;
|
||||
}
|
||||
|
||||
// List folders: LIST "" "*"
|
||||
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(listCommand, response, 30000)) {
|
||||
qWarning() << "LIST command failed:" << response;
|
||||
return folders;
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
QStringList lines = response.split('\n');
|
||||
folders = parseListResponse(lines);
|
||||
|
||||
// Logout
|
||||
conn.disconnect();
|
||||
return folders;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||
{
|
||||
// Get folder name
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return false;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// Build RFC822 message (headers + body)
|
||||
QStringList headers;
|
||||
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
|
||||
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
|
||||
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
|
||||
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
|
||||
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
|
||||
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
|
||||
headers << "MIME-Version: 1.0";
|
||||
headers << "Content-Type: text/html; charset=UTF-8";
|
||||
headers << "Content-Transfer-Encoding: 7bit";
|
||||
headers << ""; // blank line separates headers from body
|
||||
headers << item.bodyHtml(); // using bodyHtml as the body
|
||||
|
||||
QString message = headers.join("\r\n");
|
||||
QByteArray msgData = message.toUtf8();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT folder (some servers require it)
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
|
||||
|
||||
// APPEND with literal: APPEND "folder" (\Seen) {size}
|
||||
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
|
||||
qWarning() << "APPEND command initial failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the literal (message data)
|
||||
conn.sendRaw(QString::fromUtf8(msgData));
|
||||
|
||||
// Wait for the tagged response (tag + OK/NO)
|
||||
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
|
||||
qWarning() << "APPEND literal failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Seen flag
|
||||
QString seenFlag = read ? "+" : "-";
|
||||
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Seen failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Flagged flag
|
||||
QString flaggedFlag = flagged ? "+" : "-";
|
||||
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Flagged failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Update local DB
|
||||
auto items = MailItemDao::findByFolderId(fid);
|
||||
for (MailItem& item : items) {
|
||||
if (item.uid() == uid) {
|
||||
item.setRead(read);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as \\Deleted
|
||||
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Deleted failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// EXPUNGE (permanently delete)
|
||||
QString expungeCmd = "EXPUNGE";
|
||||
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "EXPUNGE failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Remove from local DB
|
||||
MailItemDao::remove(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ImapSynchronizer::generateEventId() const
|
||||
{
|
||||
return QString::number(QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
|
||||
for (const QString& line : lines) {
|
||||
QRegularExpressionMatch match = rx.match(line);
|
||||
if (match.hasMatch()) {
|
||||
QString delimiter = match.captured(1); // unused for now
|
||||
QString mailbox = match.captured(2);
|
||||
Folder folder;
|
||||
folder.setName(mailbox);
|
||||
QString lower = mailbox.toLower();
|
||||
if (lower == "inbox") folder.setInbox(true);
|
||||
else if (lower == "sent") folder.setSent(true);
|
||||
else if (lower == "drafts") folder.setDrafts(true);
|
||||
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
|
||||
folders.append(folder);
|
||||
}
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
int i = 0;
|
||||
while (i < lines.size()) {
|
||||
const QString& line = lines[i];
|
||||
if (!line.startsWith(QStringLiteral("* "))) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
|
||||
if (fetchPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract UID
|
||||
int uidPos = line.indexOf(QStringLiteral("UID "));
|
||||
if (uidPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
|
||||
if (uidEnd == -1) uidEnd = line.length();
|
||||
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
|
||||
bool ok;
|
||||
qint64 uid = uidStr.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
MailItem item;
|
||||
item.setUid(uid);
|
||||
|
||||
// Extract FLAGS
|
||||
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
|
||||
if (flagsPos != -1) {
|
||||
int flagsEnd = line.indexOf(')', flagsPos + 7);
|
||||
if (flagsEnd != -1) {
|
||||
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
||||
item.setRead(flags.contains(QStringLiteral("\\Seen")));
|
||||
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract INTERNALDATE
|
||||
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
|
||||
if (datePos != -1) {
|
||||
int dateStart = datePos + 15; // length of "INTERNALDATE \""
|
||||
int dateEnd = line.indexOf('\"', dateStart);
|
||||
if (dateEnd != -1) {
|
||||
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
|
||||
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
|
||||
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
|
||||
QString bodyData;
|
||||
int bodyPos = line.indexOf(QStringLiteral("BODY[]"));
|
||||
if (bodyPos != -1) {
|
||||
int bracePos = line.indexOf('{', bodyPos);
|
||||
if (bracePos != -1) {
|
||||
int sizeEnd = line.indexOf('}', bracePos);
|
||||
if (sizeEnd != -1) {
|
||||
bool sizeOk;
|
||||
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
|
||||
if (sizeOk) {
|
||||
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
|
||||
if (dataStart != -1) {
|
||||
dataStart += 2; // skip \r\n
|
||||
int available = line.length() - dataStart;
|
||||
if (available > size) available = size;
|
||||
bodyData = line.mid(dataStart, available);
|
||||
int received = available;
|
||||
int remaining = size - received;
|
||||
int j = i + 1;
|
||||
while (j < lines.size() && remaining > 0) {
|
||||
const QString& nextLine = lines[j];
|
||||
int take = qMin(nextLine.length(), remaining);
|
||||
bodyData += nextLine.left(take);
|
||||
remaining -= take;
|
||||
++j;
|
||||
}
|
||||
// we have consumed lines up to j-1
|
||||
i = j - 1; // will be incremented at end of loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse headers from bodyData
|
||||
if (!bodyData.isEmpty()) {
|
||||
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
|
||||
auto it = headerRx.globalMatch(bodyData);
|
||||
while (it.hasNext()) {
|
||||
auto match = it.next();
|
||||
QString key = match.captured(2).toLower().trimmed();
|
||||
QString value = match.captured(3).trimmed();
|
||||
if (key == QStringLiteral("subject"))
|
||||
item.setSubject(value);
|
||||
else if (key == QStringLiteral("from"))
|
||||
item.setSender(value);
|
||||
else if (key == QStringLiteral("to"))
|
||||
item.setRecipient(value);
|
||||
else if (key == QStringLiteral("date")) {
|
||||
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If subject not set, use placeholder
|
||||
if (item.subject().isEmpty())
|
||||
item.setSubject(QStringLiteral("(No Subject)"));
|
||||
|
||||
items.append(item);
|
||||
++i;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
#include "imapsynchronizer.h"
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
#include <QRegularExpression>
|
||||
#include "../../core/mailitem.h"
|
||||
#include "../../core/models/account.h"
|
||||
#include "../../core/models/folder.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
|
||||
ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
||||
: Synchronizer(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::initialize(const Account& account)
|
||||
{
|
||||
const Account::ConnectionSettings& settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
m_useSsl = settings.incomingSsl;
|
||||
m_username = settings.username;
|
||||
m_password = settings.password;
|
||||
m_authMethod = settings.authMethod;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper: connects and logs in to the IMAP server
|
||||
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
||||
{
|
||||
bool connected = false;
|
||||
bool loggedIn = false;
|
||||
QString response;
|
||||
|
||||
// Connect to host
|
||||
QEventLoop connectLoop;
|
||||
QTimer connectTimer;
|
||||
bool connectTimeout = false;
|
||||
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP connection error:" << err;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.setSingleShot(true);
|
||||
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
|
||||
connectTimeout = true;
|
||||
connectLoop.quit();
|
||||
});
|
||||
connectTimer.start(30000); // 30 seconds
|
||||
conn.connectToHost(m_host, m_port, m_useSsl);
|
||||
connectLoop.exec();
|
||||
if (connectTimeout) {
|
||||
qWarning() << "Connection timeout to" << m_host;
|
||||
return false;
|
||||
}
|
||||
connected = true;
|
||||
|
||||
// Wait for server greeting (first untagged response)
|
||||
QEventLoop greetLoop;
|
||||
bool gotGreeting = false;
|
||||
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
|
||||
[&](const QString& line) {
|
||||
Q_UNUSED(line);
|
||||
gotGreeting = true;
|
||||
greetLoop.quit();
|
||||
});
|
||||
QTimer greetTimer;
|
||||
greetTimer.setSingleShot(true);
|
||||
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
|
||||
greetLoop.quit();
|
||||
});
|
||||
greetTimer.start(30000); // 30 seconds
|
||||
greetLoop.exec();
|
||||
QObject::disconnect(greetConn);
|
||||
if (!gotGreeting) {
|
||||
qWarning() << "Timeout waiting for server greeting";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Login
|
||||
QEventLoop loginLoop;
|
||||
QTimer loginTimer;
|
||||
bool loginTimeout = false;
|
||||
bool loginSuccess = false;
|
||||
// Already connected, now login
|
||||
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
|
||||
if (ok) {
|
||||
loginSuccess = true;
|
||||
} else {
|
||||
qWarning() << "Login failed:" << resp;
|
||||
}
|
||||
loginLoop.quit();
|
||||
});
|
||||
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||
qWarning() << "IMAP error during login:" << err;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.setSingleShot(true);
|
||||
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
|
||||
loginTimeout = true;
|
||||
loginLoop.quit();
|
||||
});
|
||||
loginTimer.start(30000);
|
||||
loginLoop.exec();
|
||||
if (loginTimeout) {
|
||||
qWarning() << "Login timeout";
|
||||
return false;
|
||||
}
|
||||
if (!loginSuccess) {
|
||||
return false;
|
||||
}
|
||||
loggedIn = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::syncFolder(const Folder& folder)
|
||||
{
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// Select folder
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
|
||||
QString selectResp;
|
||||
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed for" << folder.name();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get all UIDs
|
||||
QString searchCmd = "UID SEARCH ALL";
|
||||
QString searchResp;
|
||||
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
|
||||
qWarning() << "SEARCH failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResp.split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no messages, set unread count to 0 and exit
|
||||
if (uids.isEmpty()) {
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(0);
|
||||
FolderDao::update(f);
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get flags for all UIDs in one command
|
||||
QStringList uidStrs;
|
||||
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
|
||||
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
|
||||
QString fetchResp;
|
||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
|
||||
qWarning() << "FETCH FLAGS failed";
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse flags and update local DB
|
||||
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
|
||||
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
|
||||
auto it = flagsRx.globalMatch(fetchResp);
|
||||
while (it.hasNext()) {
|
||||
auto m = it.next();
|
||||
qint64 uid = m.captured(1).toLongLong();
|
||||
QString flags = m.captured(2);
|
||||
bool seen = flags.contains("\\Seen");
|
||||
bool flagged = flags.contains("\\Flagged");
|
||||
flagsMap[uid] = qMakePair(seen, flagged);
|
||||
}
|
||||
|
||||
// Update local items
|
||||
auto localItems = MailItemDao::findByFolderId(folder.id());
|
||||
for (MailItem& item : localItems) {
|
||||
if (flagsMap.contains(item.uid())) {
|
||||
bool seen = flagsMap[item.uid()].first;
|
||||
bool flagged = flagsMap[item.uid()].second;
|
||||
if (item.isRead() != seen || item.isFlagged() != flagged) {
|
||||
item.setRead(seen);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count unread
|
||||
int unread = 0;
|
||||
for (const MailItem& item : localItems) {
|
||||
if (!item.isRead()) unread++;
|
||||
}
|
||||
Folder f = folder;
|
||||
f.setUnreadCount(unread);
|
||||
FolderDao::update(f);
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// Get folder name from DB
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return items;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// SELECT with real folder name
|
||||
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString selectResponse;
|
||||
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
|
||||
qWarning() << "SELECT command failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
if (!selectResponse.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed:" << selectResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// SEARCH
|
||||
QString searchCommand;
|
||||
if (sinceUid > 0) {
|
||||
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
|
||||
} else {
|
||||
searchCommand = "UID SEARCH ALL";
|
||||
}
|
||||
QString searchResponse;
|
||||
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
|
||||
qWarning() << "SEARCH command failed:" << searchResponse;
|
||||
return items;
|
||||
}
|
||||
|
||||
// Parse UIDs (robust: look for line starting with "* SEARCH")
|
||||
QVector<qint64> uids;
|
||||
QStringList lines = searchResponse.split('\n');
|
||||
for (const QString& line : lines) {
|
||||
if (line.startsWith("* SEARCH")) {
|
||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||
bool ok;
|
||||
qint64 uid = parts[i].toLongLong(&ok);
|
||||
if (ok && uid > 0) uids.append(uid);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (uids.isEmpty()) {
|
||||
conn.sendCommandWait("CLOSE", searchResponse, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
// Fetch in batches of 50 UIDs
|
||||
const int batchSize = 50;
|
||||
for (int i = 0; i < uids.size(); i += batchSize) {
|
||||
QVector<qint64> batch = uids.mid(i, qMin(batchSize, uids.size() - i));
|
||||
QString batchList;
|
||||
for (qint64 uid : batch) {
|
||||
batchList.append(QString::number(uid)).append(",");
|
||||
}
|
||||
batchList.chop(1); // remove trailing comma
|
||||
|
||||
// Fetch headers and flags (efficient)
|
||||
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
|
||||
.arg(batchList);
|
||||
QString fetchResponse;
|
||||
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
|
||||
qWarning() << "FETCH failed for batch" << i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse and persist
|
||||
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
|
||||
for (MailItem& item : batchItems) {
|
||||
item.setFolderId(fid);
|
||||
if (MailItemDao::insert(item)) {
|
||||
items.append(item);
|
||||
} else {
|
||||
qWarning() << "Failed to insert mail item uid" << item.uid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close and disconnect
|
||||
QString dummy;
|
||||
conn.sendCommandWait("CLOSE", dummy, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::getFolders() const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
ImapConnection conn;
|
||||
|
||||
if (!connectAndLogin(conn)) {
|
||||
return folders;
|
||||
}
|
||||
|
||||
// List folders: LIST "" "*"
|
||||
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(listCommand, response, 30000)) {
|
||||
qWarning() << "LIST command failed:" << response;
|
||||
return folders;
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
QStringList lines = response.split('\n');
|
||||
folders = parseListResponse(lines);
|
||||
|
||||
// Logout
|
||||
conn.disconnect();
|
||||
return folders;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||
{
|
||||
// Get folder name
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) {
|
||||
qWarning() << "Folder not found for id" << folderId;
|
||||
return false;
|
||||
}
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
// Build RFC822 message (headers + body)
|
||||
QStringList headers;
|
||||
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
|
||||
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
|
||||
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
|
||||
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
|
||||
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
|
||||
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
|
||||
headers << "MIME-Version: 1.0";
|
||||
headers << "Content-Type: text/html; charset=UTF-8";
|
||||
headers << "Content-Transfer-Encoding: 7bit";
|
||||
headers << ""; // blank line separates headers from body
|
||||
headers << item.bodyHtml(); // using bodyHtml as the body
|
||||
|
||||
QString message = headers.join("\r\n");
|
||||
QByteArray msgData = message.toUtf8();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT folder (some servers require it)
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
|
||||
|
||||
// APPEND with literal: APPEND "folder" (\Seen) {size}
|
||||
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
|
||||
QString response;
|
||||
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
|
||||
qWarning() << "APPEND command initial failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the literal (message data)
|
||||
conn.sendRaw(QString::fromUtf8(msgData));
|
||||
|
||||
// Wait for the tagged response (tag + OK/NO)
|
||||
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
|
||||
qWarning() << "APPEND literal failed:" << response;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Seen flag
|
||||
QString seenFlag = read ? "+" : "-";
|
||||
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Seen failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// STORE for Flagged flag
|
||||
QString flaggedFlag = flagged ? "+" : "-";
|
||||
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Flagged failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Update local DB
|
||||
auto items = MailItemDao::findByFolderId(fid);
|
||||
for (MailItem& item : items) {
|
||||
if (item.uid() == uid) {
|
||||
item.setRead(read);
|
||||
item.setFlagged(flagged);
|
||||
MailItemDao::update(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
|
||||
{
|
||||
qint64 uid = itemUid.toLongLong();
|
||||
if (uid <= 0) return false;
|
||||
|
||||
int fid = folderId.toInt();
|
||||
auto folderOpt = FolderDao::findById(fid);
|
||||
if (!folderOpt) return false;
|
||||
QString folderName = folderOpt->name();
|
||||
|
||||
ImapConnection conn;
|
||||
if (!connectAndLogin(conn)) return false;
|
||||
|
||||
// SELECT
|
||||
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
|
||||
QString resp;
|
||||
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "SELECT failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark as \\Deleted
|
||||
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
|
||||
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "STORE Deleted failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// EXPUNGE (permanently delete)
|
||||
QString expungeCmd = "EXPUNGE";
|
||||
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
|
||||
qWarning() << "EXPUNGE failed:" << resp;
|
||||
conn.disconnect();
|
||||
return false;
|
||||
}
|
||||
|
||||
conn.disconnect();
|
||||
|
||||
// Remove from local DB
|
||||
MailItemDao::remove(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ImapSynchronizer::generateEventId() const
|
||||
{
|
||||
return QString::number(QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
|
||||
for (const QString& line : lines) {
|
||||
QRegularExpressionMatch match = rx.match(line);
|
||||
if (match.hasMatch()) {
|
||||
QString delimiter = match.captured(1); // unused for now
|
||||
QString mailbox = match.captured(2);
|
||||
Folder folder;
|
||||
folder.setName(mailbox);
|
||||
QString lower = mailbox.toLower();
|
||||
if (lower == "inbox") folder.setInbox(true);
|
||||
else if (lower == "sent") folder.setSent(true);
|
||||
else if (lower == "drafts") folder.setDrafts(true);
|
||||
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
|
||||
folders.append(folder);
|
||||
}
|
||||
}
|
||||
return folders;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
int i = 0;
|
||||
while (i < lines.size()) {
|
||||
const QString& line = lines[i];
|
||||
if (!line.startsWith(QStringLiteral("* "))) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
|
||||
if (fetchPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract UID
|
||||
int uidPos = line.indexOf(QStringLiteral("UID "));
|
||||
if (uidPos == -1) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
|
||||
if (uidEnd == -1) uidEnd = line.length();
|
||||
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
|
||||
bool ok;
|
||||
qint64 uid = uidStr.toLongLong(&ok);
|
||||
if (!ok) {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
MailItem item;
|
||||
item.setUid(uid);
|
||||
|
||||
// Extract FLAGS
|
||||
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
|
||||
if (flagsPos != -1) {
|
||||
int flagsEnd = line.indexOf(')', flagsPos + 7);
|
||||
if (flagsEnd != -1) {
|
||||
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
||||
item.setRead(flags.contains(QStringLiteral("\\Seen")));
|
||||
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
|
||||
}
|
||||
}
|
||||
|
||||
// Extract INTERNALDATE
|
||||
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
|
||||
if (datePos != -1) {
|
||||
int dateStart = datePos + 15; // length of "INTERNALDATE \""
|
||||
int dateEnd = line.indexOf('\"', dateStart);
|
||||
if (dateEnd != -1) {
|
||||
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
|
||||
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
|
||||
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
|
||||
QString bodyData;
|
||||
int bodyPos = line.indexOf(QStringLiteral("BODY["));
|
||||
if (bodyPos != -1) {
|
||||
int bracePos = line.indexOf('{', bodyPos);
|
||||
if (bracePos != -1) {
|
||||
int sizeEnd = line.indexOf('}', bracePos);
|
||||
if (sizeEnd != -1) {
|
||||
bool sizeOk;
|
||||
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
|
||||
if (sizeOk) {
|
||||
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
|
||||
if (dataStart != -1) {
|
||||
dataStart += 2; // skip \r\n
|
||||
int available = line.length() - dataStart;
|
||||
if (available > size) available = size;
|
||||
bodyData = line.mid(dataStart, available);
|
||||
int received = available;
|
||||
int remaining = size - received;
|
||||
int j = i + 1;
|
||||
while (j < lines.size() && remaining > 0) {
|
||||
const QString& nextLine = lines[j];
|
||||
int take = qMin(nextLine.length(), remaining);
|
||||
bodyData += nextLine.left(take);
|
||||
remaining -= take;
|
||||
++j;
|
||||
}
|
||||
// we have consumed lines up to j-1
|
||||
i = j - 1; // will be incremented at end of loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse headers from bodyData
|
||||
if (!bodyData.isEmpty()) {
|
||||
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
|
||||
auto it = headerRx.globalMatch(bodyData);
|
||||
while (it.hasNext()) {
|
||||
auto match = it.next();
|
||||
QString key = match.captured(2).toLower().trimmed();
|
||||
QString value = match.captured(3).trimmed();
|
||||
if (key == QStringLiteral("subject"))
|
||||
item.setSubject(value);
|
||||
else if (key == QStringLiteral("from"))
|
||||
item.setSender(value);
|
||||
else if (key == QStringLiteral("to"))
|
||||
item.setRecipient(value);
|
||||
else if (key == QStringLiteral("date")) {
|
||||
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
|
||||
if (dt.isValid())
|
||||
item.setDate(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If subject not set, use placeholder
|
||||
if (item.subject().isEmpty())
|
||||
item.setSubject(QStringLiteral("(No Subject)"));
|
||||
|
||||
items.append(item);
|
||||
++i;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#include "../synchronizer.h"
|
||||
#include <QObject>
|
||||
#include <QSslSocket>
|
||||
#include <QStringList>
|
||||
#include "imapconnection.h"
|
||||
|
||||
class ImapSynchronizer : public Synchronizer
|
||||
{
|
||||
@@ -30,20 +30,28 @@ signals:
|
||||
void statusMessage(const QString& message) const;
|
||||
|
||||
private:
|
||||
// Connection details
|
||||
QVector<qint64> parseUidSearchResponse(const QString& response) const;
|
||||
// Connection details (set by initialize)
|
||||
mutable QString m_host;
|
||||
mutable quint16 m_port{0};
|
||||
mutable bool m_useSsl{false};
|
||||
mutable QString m_username;
|
||||
mutable QString m_password;
|
||||
mutable QString m_authMethod; // "plain" or "oauth2"
|
||||
mutable QByteArray m_capabilities;
|
||||
|
||||
// Helper para generar IDs únicos de eventos
|
||||
QString generateEventId() const;
|
||||
|
||||
// IMAP command helpers
|
||||
bool sendCommand(QSslSocket& socket, const QString& command, QString& response) const;
|
||||
bool waitForResponse(QSslSocket& socket, const QString& expectedTag, QString& response) const;
|
||||
QVector<Folder> parseListResponse(const QString& response) const;
|
||||
};
|
||||
// Helper: conecta y loguea al servidor (reutilizable)
|
||||
bool connectAndLogin(ImapConnection &conn) const;
|
||||
|
||||
// IMAP response parsers
|
||||
QVector<Folder> parseListResponse(const QStringList& lines) const;
|
||||
QVector<MailItem> parseFetchResponse(const QStringList& lines) const;
|
||||
QVector<MailItem> parseFetchResponseBytes(const QByteArray& response) const;
|
||||
bool persistFetchedItem(const Folder& folder, MailItem& item) const;
|
||||
|
||||
// Sync folder helpers
|
||||
QVector<qint64> fetchAllUids(const QString& folderId) const;
|
||||
void parseAndUpdateFlags(const QByteArray& response, int folderId) const;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "../synchronizer.h"
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include "imapconnection.h"
|
||||
|
||||
class ImapSynchronizer : public Synchronizer
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ImapSynchronizer(QObject* parent = nullptr);
|
||||
~ImapSynchronizer() override = default;
|
||||
|
||||
// Synchronizer interface
|
||||
bool initialize(const Account& account) override;
|
||||
bool syncFolder(const Folder& folder) override;
|
||||
QVector<Folder> getFolders() const override;
|
||||
QVector<MailItem> fetchMailItems(const QString& folderId,
|
||||
qint64 sinceUid = 0) override;
|
||||
bool appendMailItem(const QString& folderId, const MailItem& item) override;
|
||||
bool updateMailItemFlags(const QString& folderId,
|
||||
const QString& itemUid,
|
||||
bool read, bool flagged) override;
|
||||
bool deleteMailItem(const QString& folderId,
|
||||
const QString& itemUid) override;
|
||||
|
||||
signals:
|
||||
void progressChanged(int percent) const;
|
||||
void statusMessage(const QString& message) const;
|
||||
|
||||
private:
|
||||
// Connection details (set by initialize)
|
||||
mutable QString m_host;
|
||||
mutable quint16 m_port{0};
|
||||
mutable bool m_useSsl{false};
|
||||
mutable QString m_username;
|
||||
mutable QString m_password;
|
||||
mutable QString m_authMethod; // "plain" or "oauth2"
|
||||
|
||||
// Helper para generar IDs únicos de eventos
|
||||
QString generateEventId() const;
|
||||
|
||||
// Helper: conecta y loguea al servidor (reutilizable)
|
||||
bool connectAndLogin(ImapConnection &conn) const;
|
||||
|
||||
// IMAP response parsers
|
||||
QVector<Folder> parseListResponse(const QStringList& lines) const;
|
||||
QVector<MailItem> parseFetchResponse(const QStringList& lines) const;
|
||||
};
|
||||
+389
-11
@@ -3,6 +3,302 @@
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QDebug>
|
||||
#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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MailService::MailService(AccountService *accountService, QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -18,13 +314,43 @@ QVector<MailItem> MailService::getMails(const QString &folderId)
|
||||
|
||||
void MailService::sendMail(const MailItem &mail, const QString &accountId)
|
||||
{
|
||||
// For now we just simulate sending by using EmailComposerBridge
|
||||
// In a real implementation, this would use SMTP settings from the account.
|
||||
Q_UNUSED(mail);
|
||||
Q_UNUSED(accountId);
|
||||
// Emit success immediately; actual sending would be asynchronous.
|
||||
QMetaObject::invokeMethod(this, "mailSent", Qt::QueuedConnection,
|
||||
Q_ARG(QString, QString()));
|
||||
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);
|
||||
}
|
||||
|
||||
void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||
@@ -35,6 +361,10 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||
return;
|
||||
}
|
||||
Account *acc = m_accountService->findAccountById(accountId.toLongLong());
|
||||
if (!acc) {
|
||||
emit mailFetchError(accountId, folderId, tr("Account not found"));
|
||||
return;
|
||||
}
|
||||
QString providerType = QStringLiteral("imap"); // fallback
|
||||
if (acc) {
|
||||
switch (acc->type()) {
|
||||
@@ -65,10 +395,12 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||
|
||||
// Initialize synchronizer with account details before using it
|
||||
if (!sync->initialize(*acc)) {
|
||||
delete acc;
|
||||
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
|
||||
sync->deleteLater();
|
||||
SynchronizerProvider::instance().unregisterSynchronizer(accountId);
|
||||
return;
|
||||
}
|
||||
delete acc;
|
||||
|
||||
// Connect progress signals (if they exist) using string-based syntax for compatibility
|
||||
QObject::connect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)), Qt::QueuedConnection);
|
||||
@@ -85,12 +417,52 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||
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();
|
||||
QVector<MailItem> persisted;
|
||||
for (MailItem &item : items) {
|
||||
if (persistFetchedItem(item, accountId, folderId)) persisted.append(item);
|
||||
}
|
||||
watcher->deleteLater();
|
||||
emit mailFetched(accountId, folderId, items);
|
||||
emit mailFetched(accountId, folderId, persisted);
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
|
||||
{
|
||||
bool ok;
|
||||
@@ -109,8 +481,14 @@ void MailService::deleteMail(const QString &mailItemId)
|
||||
bool ok;
|
||||
qint64 id = mailItemId.toLongLong(&ok);
|
||||
if (!ok) return;
|
||||
if (MailItemDao::remove(id))
|
||||
const std::optional<MailItem> item = MailItemDao::findById(id);
|
||||
if (MailItemDao::remove(id)) {
|
||||
if (item && !item->fileId().isEmpty()) {
|
||||
MimeStorageService storage;
|
||||
storage.deleteEmlFile(item->fileId());
|
||||
}
|
||||
emit mailDeleted(mailItemId);
|
||||
}
|
||||
}
|
||||
|
||||
void MailService::markAsRead(const QString &mailItemId, bool read)
|
||||
@@ -126,4 +504,4 @@ void MailService::markAsRead(const QString &mailItemId, bool read)
|
||||
emit mailReadStateChanged(mailItemId, read);
|
||||
}
|
||||
|
||||
#include "mailservice.moc"
|
||||
#include "mailservice.moc"
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#include "mailservice.h"
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QDebug>
|
||||
|
||||
MailService::MailService(AccountService *accountService, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_composer(new EmailComposerBridge(this))
|
||||
, m_accountService(accountService)
|
||||
{
|
||||
}
|
||||
|
||||
QVector<MailItem> MailService::getMails(const QString &folderId)
|
||||
{
|
||||
return MailItemDao::findByFolderId(folderId.toInt());
|
||||
}
|
||||
|
||||
void MailService::sendMail(const MailItem &mail, const QString &accountId)
|
||||
{
|
||||
// For now we just simulate sending by using EmailComposerBridge
|
||||
// In a real implementation, this would use SMTP settings from the account.
|
||||
Q_UNUSED(mail);
|
||||
Q_UNUSED(accountId);
|
||||
// Emit success immediately; actual sending would be asynchronous.
|
||||
QMetaObject::invokeMethod(this, "mailSent", Qt::QueuedConnection,
|
||||
Q_ARG(QString, QString()));
|
||||
}
|
||||
|
||||
void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||
{
|
||||
// Determine provider type from account
|
||||
if (!m_accountService) {
|
||||
emit mailFetchError(accountId, folderId, tr("AccountService not available"));
|
||||
return;
|
||||
}
|
||||
Account *acc = m_accountService->findAccountById(accountId.toLongLong());
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// Initialize synchronizer with account details before using it
|
||||
if (!sync->initialize(*acc)) {
|
||||
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
|
||||
sync->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
QFuture<QVector<MailItem>> future = QtConcurrent::run([=]() {
|
||||
// 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);
|
||||
QObject::connect(watcher, &QFutureWatcher<QVector<MailItem>>::finished, this, [=]() {
|
||||
// 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();
|
||||
watcher->deleteLater();
|
||||
emit mailFetched(accountId, folderId, items);
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
|
||||
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
void MailService::deleteMail(const QString &mailItemId)
|
||||
{
|
||||
bool ok;
|
||||
qint64 id = mailItemId.toLongLong(&ok);
|
||||
if (!ok) return;
|
||||
if (MailItemDao::remove(id))
|
||||
emit mailDeleted(mailItemId);
|
||||
}
|
||||
|
||||
void MailService::markAsRead(const QString &mailItemId, bool read)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
#include "mailservice.moc"
|
||||
@@ -22,6 +22,8 @@ public:
|
||||
|
||||
QVector<MailItem> getMails(const QString &folderId);
|
||||
void sendMail(const MailItem &mail, const QString &accountId);
|
||||
void sendMail(const MailItem &mail, const QString &accountId,
|
||||
const QStringList &attachmentPaths);
|
||||
void fetchMails(const QString &accountId, const QString &folderId);
|
||||
void moveMail(const QString &mailItemId, const QString &targetFolderId);
|
||||
void deleteMail(const QString &mailItemId);
|
||||
@@ -40,8 +42,10 @@ signals:
|
||||
void statusMessage(const QString &message);
|
||||
|
||||
private:
|
||||
bool persistFetchedItem(MailItem &item, const QString &accountId,
|
||||
const QString &folderId);
|
||||
EmailComposerBridge *m_composer;
|
||||
AccountService *m_accountService;
|
||||
};
|
||||
|
||||
#endif // MAILSERVICE_H
|
||||
#endif // MAILSERVICE_H
|
||||
|
||||
+296
-53
@@ -2,6 +2,14 @@
|
||||
#include <QStandardPaths>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QCryptographicHash>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextStream>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QSaveFile>
|
||||
#include <QMap>
|
||||
|
||||
MimeStorageService::MimeStorageService(QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -23,53 +31,99 @@ QString MimeStorageService::ensureDirectory(const QString &path) const
|
||||
return dir.absolutePath();
|
||||
}
|
||||
|
||||
QString MimeStorageService::safeComponent(const QString &value) const
|
||||
{
|
||||
QString result = value;
|
||||
result.replace(QRegularExpression(QStringLiteral("[^A-Za-z0-9._-]")), QStringLiteral("_"));
|
||||
if (result.isEmpty() || result == QStringLiteral(".") || result == QStringLiteral(".."))
|
||||
result = QStringLiteral("unknown");
|
||||
return result.left(180);
|
||||
}
|
||||
|
||||
QString MimeStorageService::saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail)
|
||||
{
|
||||
QString dir = storagePath() + "/" + accountId + "/" + folderId;
|
||||
ensureDirectory(dir);
|
||||
QByteArray raw;
|
||||
raw.append("From: ").append(mail.sender().toUtf8()).append("\r\n");
|
||||
raw.append("To: ").append(mail.recipient().toUtf8()).append("\r\n");
|
||||
if (!mail.cc().isEmpty()) raw.append("Cc: ").append(mail.cc().toUtf8()).append("\r\n");
|
||||
if (!mail.bcc().isEmpty()) raw.append("Bcc: ").append(mail.bcc().toUtf8()).append("\r\n");
|
||||
raw.append("Subject: ").append(mail.subject().toUtf8()).append("\r\n");
|
||||
raw.append("Date: ").append(mail.date().toString(Qt::RFC2822Date).toUtf8()).append("\r\n");
|
||||
raw.append("MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n");
|
||||
raw.append(mail.bodyHtml().toUtf8()).append("\r\n");
|
||||
return saveRawEmlFile(accountId, folderId, raw, mail.messageId());
|
||||
}
|
||||
|
||||
QString fileName = mail.messageId();
|
||||
if (fileName.isEmpty()) {
|
||||
fileName = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
}
|
||||
fileName.replace('/', '_').replace('\\', '_');
|
||||
QString filePath = dir + "/" + fileName + ".eml";
|
||||
QString MimeStorageService::saveRawEmlFile(const QString &accountId, const QString &folderId,
|
||||
const QByteArray &rawMime, const QString &stableId)
|
||||
{
|
||||
if (rawMime.isEmpty()) return QString();
|
||||
|
||||
// Write a basic .eml structure
|
||||
QFile file(filePath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "From: " << mail.sender() << "\r\n";
|
||||
stream << "To: " << mail.recipient() << "\r\n";
|
||||
stream << "Subject: " << mail.subject() << "\r\n";
|
||||
stream << "Date: " << mail.date().toString(Qt::RFC2822Date) << "\r\n";
|
||||
stream << "MIME-Version: 1.0\r\n";
|
||||
stream << "Content-Type: text/html; charset=UTF-8\r\n";
|
||||
stream << "\r\n";
|
||||
stream << mail.bodyHtml() << "\r\n";
|
||||
file.close();
|
||||
qDebug() << "[MimeStorage] Saved .eml:" << filePath;
|
||||
return fileName;
|
||||
const QString accountPart = safeComponent(accountId);
|
||||
const QString folderPart = safeComponent(folderId);
|
||||
const QByteArray digest = QCryptographicHash::hash(rawMime, QCryptographicHash::Sha256).toHex();
|
||||
const QString fileName = safeComponent(stableId.isEmpty()
|
||||
? QString::fromLatin1(digest)
|
||||
: stableId) + QStringLiteral("_") + QString::fromLatin1(digest.left(12)) + QStringLiteral(".eml");
|
||||
const QString relative = accountPart + "/" + folderPart + "/" + fileName;
|
||||
const QString path = QDir(storagePath()).filePath(relative);
|
||||
ensureDirectory(QFileInfo(path).absolutePath());
|
||||
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(rawMime) != rawMime.size() || !file.commit()) {
|
||||
qWarning() << "[MimeStorage] Failed to save raw MIME:" << path << file.errorString();
|
||||
return QString();
|
||||
}
|
||||
qWarning() << "[MimeStorage] Failed to save .eml:" << filePath;
|
||||
return QString();
|
||||
return relative.left(relative.size() - 4); // fileId excludes .eml
|
||||
}
|
||||
|
||||
bool MimeStorageService::storeMessage(const QString &accountId, const QString &folderId,
|
||||
MailItem &mail, const QByteArray &rawMime,
|
||||
QVector<QString> *storedAttachmentPaths)
|
||||
{
|
||||
ParsedMimeMessage parsed;
|
||||
if (!parseMessage(rawMime, parsed)) return false;
|
||||
|
||||
if (!parsed.messageId.isEmpty()) mail.setMessageId(parsed.messageId);
|
||||
if (!parsed.subject.isEmpty()) mail.setSubject(parsed.subject);
|
||||
if (!parsed.from.isEmpty()) mail.setSender(parsed.from);
|
||||
if (!parsed.to.isEmpty()) { mail.setTo(parsed.to); mail.setRecipient(parsed.to); }
|
||||
if (!parsed.cc.isEmpty()) mail.setCc(parsed.cc);
|
||||
if (!parsed.bcc.isEmpty()) mail.setBcc(parsed.bcc);
|
||||
if (parsed.date.isValid()) mail.setDate(parsed.date);
|
||||
mail.setBodyHtml(parsed.bodyHtml);
|
||||
mail.setSize(rawMime.size());
|
||||
mail.setRawMime(rawMime);
|
||||
|
||||
const QString fileId = saveRawEmlFile(accountId, folderId, rawMime, mail.messageId());
|
||||
if (fileId.isEmpty()) return false;
|
||||
mail.setFileId(fileId);
|
||||
|
||||
QVector<QString> names;
|
||||
QVector<QString> savedPaths;
|
||||
for (const ParsedMimeAttachment &attachment : parsed.attachments) {
|
||||
const QString path = saveAttachment(fileId, attachment.fileName, attachment.data);
|
||||
if (path.isEmpty()) {
|
||||
for (const QString &savedPath : savedPaths) QFile::remove(savedPath);
|
||||
deleteEmlFile(fileId);
|
||||
return false;
|
||||
}
|
||||
savedPaths.append(path);
|
||||
names.append(attachment.fileName);
|
||||
if (storedAttachmentPaths) storedAttachmentPaths->append(path);
|
||||
}
|
||||
mail.setAttachments(names);
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MimeStorageService::getEmlFilePath(const QString &fileId) const
|
||||
{
|
||||
// Search in all account/folder directories (simplified)
|
||||
QDir baseDir(storagePath());
|
||||
QStringList filters;
|
||||
filters << fileId + ".eml";
|
||||
|
||||
QFileInfoList files = baseDir.entryInfoList(filters, QDir::Files, QDir::Name);
|
||||
for (const auto &info : files) {
|
||||
QStringList parts = info.absoluteFilePath().split('/');
|
||||
if (parts.size() >= 2) {
|
||||
return info.absoluteFilePath();
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
QString relative = fileId;
|
||||
if (relative.endsWith(QStringLiteral(".eml"))) relative.chop(4);
|
||||
const QString base = QDir::cleanPath(storagePath());
|
||||
const QString path = QDir::cleanPath(QDir(base).filePath(relative + QStringLiteral(".eml")));
|
||||
if (!path.startsWith(base + QDir::separator()) || !QFileInfo::exists(path)) return QString();
|
||||
return path;
|
||||
}
|
||||
|
||||
QByteArray MimeStorageService::readEmlFile(const QString &fileId) const
|
||||
@@ -88,29 +142,218 @@ bool MimeStorageService::deleteEmlFile(const QString &fileId)
|
||||
{
|
||||
QString path = getEmlFilePath(fileId);
|
||||
if (path.isEmpty()) return false;
|
||||
return QFile::remove(path);
|
||||
const bool removed = QFile::remove(path);
|
||||
QDir attachments(QFileInfo(path).absolutePath() + QStringLiteral("/attachments"));
|
||||
if (attachments.exists()) attachments.removeRecursively();
|
||||
return removed;
|
||||
}
|
||||
|
||||
QStringList MimeStorageService::listAttachments(const QString &mailItemId) const
|
||||
{
|
||||
Q_UNUSED(mailItemId);
|
||||
// TODO: Parse .eml to get attachments when GMime is available
|
||||
return {};
|
||||
const QString emlPath = getEmlFilePath(mailItemId);
|
||||
if (emlPath.isEmpty()) return {};
|
||||
QDir dir(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
|
||||
return dir.entryList(QDir::Files, QDir::Name);
|
||||
}
|
||||
|
||||
bool MimeStorageService::saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data)
|
||||
QString MimeStorageService::saveAttachment(const QString &fileId, const QString &fileName, const QByteArray &data)
|
||||
{
|
||||
Q_UNUSED(mailItemId);
|
||||
QString dir = storagePath() + "/attachments";
|
||||
ensureDirectory(dir);
|
||||
|
||||
QFile file(dir + "/" + fileName);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(data);
|
||||
file.close();
|
||||
return true;
|
||||
const QString emlPath = getEmlFilePath(fileId);
|
||||
if (emlPath.isEmpty()) return QString();
|
||||
const QString dir = ensureDirectory(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
|
||||
const QString originalName = QFileInfo(fileName).fileName();
|
||||
const QString safeName = safeComponent(originalName.isEmpty()
|
||||
? QStringLiteral("attachment") : originalName);
|
||||
QString path = QDir(dir).filePath(safeName);
|
||||
if (QFileInfo::exists(path)) {
|
||||
const QFileInfo info(path);
|
||||
const QString suffix = info.completeSuffix().isEmpty()
|
||||
? QString() : QStringLiteral(".") + info.completeSuffix();
|
||||
const QString stem = info.completeBaseName();
|
||||
path = QDir(dir).filePath(stem + QStringLiteral("_")
|
||||
+ QString::fromLatin1(QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex().left(12))
|
||||
+ suffix);
|
||||
}
|
||||
return false;
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) return QString();
|
||||
return path;
|
||||
}
|
||||
|
||||
QString MimeStorageService::decodeHeaderValue(const QString &value) const
|
||||
{
|
||||
QString result = value;
|
||||
const QRegularExpression rx(QStringLiteral("=\\?([^?]+)\\?([bBqQ])\\?([^?]+)\\?="));
|
||||
QRegularExpressionMatchIterator it = rx.globalMatch(result);
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch match = it.next();
|
||||
QByteArray encoded = match.captured(3).toLatin1();
|
||||
QByteArray decoded;
|
||||
if (match.captured(2).compare(QStringLiteral("b"), Qt::CaseInsensitive) == 0)
|
||||
decoded = QByteArray::fromBase64(encoded);
|
||||
else {
|
||||
encoded.replace('_', ' ');
|
||||
QByteArray out;
|
||||
for (int i = 0; i < encoded.size(); ++i) {
|
||||
if (encoded[i] == '=' && i + 2 < encoded.size()) {
|
||||
bool ok = false;
|
||||
const int value = QByteArray(encoded.mid(i + 1, 2)).toInt(&ok, 16);
|
||||
if (ok) { out.append(char(value)); i += 2; continue; }
|
||||
}
|
||||
out.append(encoded[i]);
|
||||
}
|
||||
decoded = out;
|
||||
}
|
||||
QString decodedText = QString::fromUtf8(decoded);
|
||||
if (decodedText.isEmpty() && !decoded.isEmpty()) decodedText = QString::fromLatin1(decoded);
|
||||
result.replace(match.captured(0), decodedText);
|
||||
}
|
||||
return result.trimmed();
|
||||
}
|
||||
|
||||
QByteArray MimeStorageService::decodeTransfer(const QByteArray &data, const QString &encoding) const
|
||||
{
|
||||
const QString normalized = encoding.trimmed().toLower();
|
||||
if (normalized == QStringLiteral("base64")) return QByteArray::fromBase64(data);
|
||||
if (normalized != QStringLiteral("quoted-printable")) return data;
|
||||
|
||||
QByteArray result;
|
||||
for (int i = 0; i < data.size(); ++i) {
|
||||
if (data[i] == '=' && i + 1 < data.size()) {
|
||||
if (data[i + 1] == '\r' && i + 2 < data.size() && data[i + 2] == '\n') { i += 2; continue; }
|
||||
if (data[i + 1] == '\n') { ++i; continue; }
|
||||
if (i + 2 < data.size()) {
|
||||
bool ok = false;
|
||||
const int value = QByteArray(data.mid(i + 1, 2)).toInt(&ok, 16);
|
||||
if (ok) { result.append(char(value)); i += 2; continue; }
|
||||
}
|
||||
}
|
||||
result.append(data[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void MimeStorageService::parseMimePart(const QByteArray &part, ParsedMimeMessage &message,
|
||||
QString *plainText, QString *htmlText) const
|
||||
{
|
||||
const int crlf = part.indexOf("\r\n\r\n");
|
||||
const int lf = part.indexOf("\n\n");
|
||||
int split = crlf >= 0 ? crlf : lf;
|
||||
const int separatorSize = crlf >= 0 ? 4 : 2;
|
||||
const QByteArray headerBytes = split >= 0 ? part.left(split) : part;
|
||||
const QByteArray body = split >= 0 ? part.mid(split + separatorSize) : QByteArray();
|
||||
|
||||
QMap<QString, QString> headers;
|
||||
QString current;
|
||||
for (QString line : QString::fromLatin1(headerBytes).split(QRegularExpression("\\r?\\n"))) {
|
||||
if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) {
|
||||
headers[current] += QStringLiteral(" ") + line.trimmed();
|
||||
continue;
|
||||
}
|
||||
const int colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
current = line.left(colon).trimmed().toLower();
|
||||
headers[current] = line.mid(colon + 1).trimmed();
|
||||
}
|
||||
auto header = [&](const QString &name) { return headers.value(name.toLower()); };
|
||||
const QString contentType = header(QStringLiteral("content-type"));
|
||||
const QString disposition = header(QStringLiteral("content-disposition"));
|
||||
const QString transfer = header(QStringLiteral("content-transfer-encoding"));
|
||||
|
||||
QRegularExpression boundaryRx(QStringLiteral("(?:^|;)\\s*boundary\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption);
|
||||
const QRegularExpressionMatch boundaryMatch = boundaryRx.match(contentType);
|
||||
if (contentType.startsWith(QStringLiteral("multipart/"), Qt::CaseInsensitive) && boundaryMatch.hasMatch()) {
|
||||
const QString boundary = boundaryMatch.captured(1).isEmpty() ? boundaryMatch.captured(2) : boundaryMatch.captured(1);
|
||||
const QByteArray marker = QByteArrayLiteral("--") + boundary.toUtf8();
|
||||
int pos = 0;
|
||||
while ((pos = body.indexOf(marker, pos)) >= 0) {
|
||||
int start = pos + marker.size();
|
||||
if (body.mid(start, 2) == QByteArrayLiteral("--")) break;
|
||||
if (body.mid(start, 2) == QByteArrayLiteral("\r\n")) start += 2;
|
||||
else if (body.mid(start, 1) == QByteArrayLiteral("\n")) ++start;
|
||||
int end = body.indexOf(marker, start);
|
||||
if (end < 0) end = body.size();
|
||||
QByteArray child = body.mid(start, end - start);
|
||||
while (child.endsWith("\r\n") || child.endsWith('\n')) child.chop(child.endsWith("\r\n") ? 2 : 1);
|
||||
parseMimePart(child, message, plainText, htmlText);
|
||||
pos = end;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray decoded = decodeTransfer(body, transfer);
|
||||
QRegularExpression parameterRx(QStringLiteral("(?:^|;)\\s*(?:filename|name)(?:\\*\\d+)?\\*?\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption);
|
||||
const QRegularExpressionMatch dispositionMatch = parameterRx.match(disposition);
|
||||
const QRegularExpressionMatch typeMatch = parameterRx.match(contentType);
|
||||
QString fileName = dispositionMatch.hasMatch()
|
||||
? (dispositionMatch.captured(1).isEmpty() ? dispositionMatch.captured(2) : dispositionMatch.captured(1))
|
||||
: (typeMatch.hasMatch() ? (typeMatch.captured(1).isEmpty() ? typeMatch.captured(2) : typeMatch.captured(1)) : QString());
|
||||
const int encodedPrefix = fileName.indexOf(QStringLiteral("''"));
|
||||
if (encodedPrefix >= 0) fileName = fileName.mid(encodedPrefix + 2);
|
||||
fileName = QUrl::fromPercentEncoding(fileName.toUtf8());
|
||||
fileName = decodeHeaderValue(fileName);
|
||||
const QString mimeType = contentType.section(';', 0, 0).trimmed().toLower();
|
||||
const bool isAttachment = disposition.startsWith(QStringLiteral("attachment"), Qt::CaseInsensitive)
|
||||
|| !fileName.isEmpty() || (!mimeType.startsWith(QStringLiteral("text/")) && !mimeType.isEmpty());
|
||||
|
||||
if (isAttachment) {
|
||||
ParsedMimeAttachment attachment;
|
||||
attachment.fileName = fileName.isEmpty() ? QStringLiteral("attachment-%1").arg(message.attachments.size() + 1) : fileName;
|
||||
attachment.mimeType = mimeType;
|
||||
attachment.contentId = header(QStringLiteral("content-id")).trimmed();
|
||||
attachment.contentId.remove('<');
|
||||
attachment.contentId.remove('>');
|
||||
attachment.data = decoded;
|
||||
message.attachments.append(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
const QString text = QString::fromUtf8(decoded);
|
||||
if (mimeType == QStringLiteral("text/html")) {
|
||||
if (htmlText && htmlText->isEmpty()) *htmlText = text;
|
||||
} else if (mimeType == QStringLiteral("text/plain") || mimeType.isEmpty()) {
|
||||
if (plainText && plainText->isEmpty()) *plainText = text;
|
||||
}
|
||||
}
|
||||
|
||||
bool MimeStorageService::parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const
|
||||
{
|
||||
if (rawMime.isEmpty()) return false;
|
||||
message = ParsedMimeMessage();
|
||||
QString plainText;
|
||||
QString htmlText;
|
||||
parseMimePart(rawMime, message, &plainText, &htmlText);
|
||||
|
||||
auto parseHeaders = [&](const QByteArray &data) {
|
||||
const int split = data.indexOf("\r\n\r\n") >= 0 ? data.indexOf("\r\n\r\n") : data.indexOf("\n\n");
|
||||
const QByteArray headerData = split >= 0 ? data.left(split) : data;
|
||||
QMap<QString, QString> headers;
|
||||
QString current;
|
||||
for (QString line : QString::fromLatin1(headerData).split(QRegularExpression("\\r?\\n"))) {
|
||||
if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) { headers[current] += " " + line.trimmed(); continue; }
|
||||
const int colon = line.indexOf(':');
|
||||
if (colon < 0) continue;
|
||||
current = line.left(colon).trimmed().toLower();
|
||||
headers[current] = line.mid(colon + 1).trimmed();
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
const QMap<QString, QString> headers = parseHeaders(rawMime);
|
||||
message.messageId = decodeHeaderValue(headers.value(QStringLiteral("message-id"))).remove('<').remove('>');
|
||||
message.subject = decodeHeaderValue(headers.value(QStringLiteral("subject")));
|
||||
message.from = decodeHeaderValue(headers.value(QStringLiteral("from")));
|
||||
message.to = decodeHeaderValue(headers.value(QStringLiteral("to")));
|
||||
message.cc = decodeHeaderValue(headers.value(QStringLiteral("cc")));
|
||||
message.bcc = decodeHeaderValue(headers.value(QStringLiteral("bcc")));
|
||||
message.date = QDateTime::fromString(headers.value(QStringLiteral("date")), Qt::RFC2822Date);
|
||||
if (!message.date.isValid()) message.date = QDateTime::currentDateTimeUtc();
|
||||
if (!htmlText.isEmpty()) message.bodyHtml = htmlText;
|
||||
else {
|
||||
QString escaped = plainText.toHtmlEscaped();
|
||||
escaped.replace(QStringLiteral("\r\n"), QStringLiteral("<br>"));
|
||||
escaped.replace(QStringLiteral("\n"), QStringLiteral("<br>"));
|
||||
message.bodyHtml = escaped;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#include "mimestorage.moc"
|
||||
|
||||
@@ -5,8 +5,31 @@
|
||||
#include <QString>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QByteArray>
|
||||
#include <QVector>
|
||||
#include "core/mailitem.h"
|
||||
|
||||
struct ParsedMimeAttachment
|
||||
{
|
||||
QString fileName;
|
||||
QString mimeType;
|
||||
QString contentId;
|
||||
QByteArray data;
|
||||
};
|
||||
|
||||
struct ParsedMimeMessage
|
||||
{
|
||||
QString messageId;
|
||||
QString subject;
|
||||
QString from;
|
||||
QString to;
|
||||
QString cc;
|
||||
QString bcc;
|
||||
QDateTime date;
|
||||
QString bodyHtml;
|
||||
QVector<ParsedMimeAttachment> attachments;
|
||||
};
|
||||
|
||||
class MimeStorageService : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -15,16 +38,28 @@ public:
|
||||
~MimeStorageService() override = default;
|
||||
|
||||
QString saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail);
|
||||
QString saveRawEmlFile(const QString &accountId, const QString &folderId,
|
||||
const QByteArray &rawMime, const QString &stableId = QString());
|
||||
bool storeMessage(const QString &accountId, const QString &folderId,
|
||||
MailItem &mail, const QByteArray &rawMime,
|
||||
QVector<QString> *storedAttachmentPaths = nullptr);
|
||||
bool parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const;
|
||||
QString getEmlFilePath(const QString &fileId) const;
|
||||
QByteArray readEmlFile(const QString &fileId) const;
|
||||
bool deleteEmlFile(const QString &fileId);
|
||||
|
||||
QStringList listAttachments(const QString &mailItemId) const;
|
||||
bool saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data);
|
||||
QString saveAttachment(const QString &fileId, const QString &fileName,
|
||||
const QByteArray &data);
|
||||
|
||||
private:
|
||||
QString storagePath() const;
|
||||
QString ensureDirectory(const QString &path) const;
|
||||
QString safeComponent(const QString &value) const;
|
||||
QString decodeHeaderValue(const QString &value) const;
|
||||
QByteArray decodeTransfer(const QByteArray &data, const QString &encoding) const;
|
||||
void parseMimePart(const QByteArray &part, ParsedMimeMessage &message,
|
||||
QString *plainText, QString *htmlText) const;
|
||||
};
|
||||
|
||||
#endif // MIMESTORAGESERVICE_H
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/****************************************************************************
|
||||
** Meta object code from reading C++ file 'accountservice.h'
|
||||
**
|
||||
** Created by: The Qt Meta Object Compiler version 69 (Qt 6.10.2)
|
||||
**
|
||||
** WARNING! All changes made in this file will be lost!
|
||||
*****************************************************************************/
|
||||
|
||||
#include "accountservice.h"
|
||||
#include <QtCore/qmetatype.h>
|
||||
|
||||
#include <QtCore/qtmochelpers.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
||||
#include <QtCore/qxptype_traits.h>
|
||||
#if !defined(Q_MOC_OUTPUT_REVISION)
|
||||
#error "The header file 'accountservice.h' doesn't include <QObject>."
|
||||
#elif Q_MOC_OUTPUT_REVISION != 69
|
||||
#error "This file was generated using the moc from 6.10.2. It"
|
||||
#error "cannot be used with the include files from this version of Qt."
|
||||
#error "(The moc has changed too much.)"
|
||||
#endif
|
||||
|
||||
#ifndef Q_CONSTINIT
|
||||
#define Q_CONSTINIT
|
||||
#endif
|
||||
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
QT_WARNING_DISABLE_GCC("-Wuseless-cast")
|
||||
namespace {
|
||||
struct qt_meta_tag_ZN14AccountServiceE_t {};
|
||||
} // unnamed namespace
|
||||
|
||||
template <> constexpr inline auto AccountService::qt_create_metaobjectdata<qt_meta_tag_ZN14AccountServiceE_t>()
|
||||
{
|
||||
namespace QMC = QtMocConstants;
|
||||
QtMocHelpers::StringRefStorage qt_stringData {
|
||||
"AccountService",
|
||||
"accountListChanged",
|
||||
"",
|
||||
"accountAdded",
|
||||
"Account",
|
||||
"account",
|
||||
"accountRemoved",
|
||||
"accountId",
|
||||
"authenticationRequired",
|
||||
"email",
|
||||
"authUrl"
|
||||
};
|
||||
|
||||
QtMocHelpers::UintData qt_methods {
|
||||
// Signal 'accountListChanged'
|
||||
QtMocHelpers::SignalData<void()>(1, 2, QMC::AccessPublic, QMetaType::Void),
|
||||
// Signal 'accountAdded'
|
||||
QtMocHelpers::SignalData<void(const Account &)>(3, 2, QMC::AccessPublic, QMetaType::Void, {{
|
||||
{ 0x80000000 | 4, 5 },
|
||||
}}),
|
||||
// Signal 'accountRemoved'
|
||||
QtMocHelpers::SignalData<void(int)>(6, 2, QMC::AccessPublic, QMetaType::Void, {{
|
||||
{ QMetaType::Int, 7 },
|
||||
}}),
|
||||
// Signal 'authenticationRequired'
|
||||
QtMocHelpers::SignalData<void(const QString &, const QString &)>(8, 2, QMC::AccessPublic, QMetaType::Void, {{
|
||||
{ QMetaType::QString, 9 }, { QMetaType::QString, 10 },
|
||||
}}),
|
||||
};
|
||||
QtMocHelpers::UintData qt_properties {
|
||||
};
|
||||
QtMocHelpers::UintData qt_enums {
|
||||
};
|
||||
return QtMocHelpers::metaObjectData<AccountService, qt_meta_tag_ZN14AccountServiceE_t>(QMC::MetaObjectFlag{}, qt_stringData,
|
||||
qt_methods, qt_properties, qt_enums);
|
||||
}
|
||||
Q_CONSTINIT const QMetaObject AccountService::staticMetaObject = { {
|
||||
QMetaObject::SuperData::link<QObject::staticMetaObject>(),
|
||||
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.stringdata,
|
||||
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.data,
|
||||
qt_static_metacall,
|
||||
nullptr,
|
||||
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN14AccountServiceE_t>.metaTypes,
|
||||
nullptr
|
||||
} };
|
||||
|
||||
void AccountService::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
|
||||
{
|
||||
auto *_t = static_cast<AccountService *>(_o);
|
||||
if (_c == QMetaObject::InvokeMetaMethod) {
|
||||
switch (_id) {
|
||||
case 0: _t->accountListChanged(); break;
|
||||
case 1: _t->accountAdded((*reinterpret_cast<std::add_pointer_t<Account>>(_a[1]))); break;
|
||||
case 2: _t->accountRemoved((*reinterpret_cast<std::add_pointer_t<int>>(_a[1]))); break;
|
||||
case 3: _t->authenticationRequired((*reinterpret_cast<std::add_pointer_t<QString>>(_a[1])),(*reinterpret_cast<std::add_pointer_t<QString>>(_a[2]))); break;
|
||||
default: ;
|
||||
}
|
||||
}
|
||||
if (_c == QMetaObject::IndexOfMethod) {
|
||||
if (QtMocHelpers::indexOfMethod<void (AccountService::*)()>(_a, &AccountService::accountListChanged, 0))
|
||||
return;
|
||||
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(const Account & )>(_a, &AccountService::accountAdded, 1))
|
||||
return;
|
||||
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(int )>(_a, &AccountService::accountRemoved, 2))
|
||||
return;
|
||||
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(const QString & , const QString & )>(_a, &AccountService::authenticationRequired, 3))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const QMetaObject *AccountService::metaObject() const
|
||||
{
|
||||
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
|
||||
}
|
||||
|
||||
void *AccountService::qt_metacast(const char *_clname)
|
||||
{
|
||||
if (!_clname) return nullptr;
|
||||
if (!strcmp(_clname, qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.strings))
|
||||
return static_cast<void*>(this);
|
||||
return QObject::qt_metacast(_clname);
|
||||
}
|
||||
|
||||
int AccountService::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
|
||||
{
|
||||
_id = QObject::qt_metacall(_c, _id, _a);
|
||||
if (_id < 0)
|
||||
return _id;
|
||||
if (_c == QMetaObject::InvokeMetaMethod) {
|
||||
if (_id < 4)
|
||||
qt_static_metacall(this, _c, _id, _a);
|
||||
_id -= 4;
|
||||
}
|
||||
if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
|
||||
if (_id < 4)
|
||||
*reinterpret_cast<QMetaType *>(_a[0]) = QMetaType();
|
||||
_id -= 4;
|
||||
}
|
||||
return _id;
|
||||
}
|
||||
|
||||
// SIGNAL 0
|
||||
void AccountService::accountListChanged()
|
||||
{
|
||||
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
|
||||
}
|
||||
|
||||
// SIGNAL 1
|
||||
void AccountService::accountAdded(const Account & _t1)
|
||||
{
|
||||
QMetaObject::activate<void>(this, &staticMetaObject, 1, nullptr, _t1);
|
||||
}
|
||||
|
||||
// SIGNAL 2
|
||||
void AccountService::accountRemoved(int _t1)
|
||||
{
|
||||
QMetaObject::activate<void>(this, &staticMetaObject, 2, nullptr, _t1);
|
||||
}
|
||||
|
||||
// SIGNAL 3
|
||||
void AccountService::authenticationRequired(const QString & _t1, const QString & _t2)
|
||||
{
|
||||
QMetaObject::activate<void>(this, &staticMetaObject, 3, nullptr, _t1, _t2);
|
||||
}
|
||||
QT_WARNING_POP
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <QEventLoop>
|
||||
#include "../../core/events.h"
|
||||
#include "../../core/eventbus.h"
|
||||
#include "db/dao/folderdao.h"
|
||||
#include "services/mimestorage.h"
|
||||
|
||||
OutlookSynchronizer::OutlookSynchronizer(QObject* parent)
|
||||
: Synchronizer(parent),
|
||||
@@ -84,7 +86,6 @@ bool OutlookSynchronizer::syncFolder(const Folder& folder)
|
||||
// En una implementación real, aquí compararíamos con la base de datos local
|
||||
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
|
||||
|
||||
// Por ahora, simulamos que obtenemos algunos elementos
|
||||
if (!items.isEmpty()) {
|
||||
for (const MailItem& item : items) {
|
||||
emit mailItemAdded(item);
|
||||
@@ -139,128 +140,123 @@ QVector<MailItem> OutlookSynchronizer::fetchMailItems(const QString& folderId,
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Fetching mail items for folder:" << folderId;
|
||||
|
||||
// Construir la URL para Microsoft Graph API
|
||||
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
|
||||
QString url = buildGraphUrl(endpoint);
|
||||
|
||||
// Parámetros de consulta
|
||||
qDebug() << "Fetching complete Outlook messages for folder:" << folderId;
|
||||
|
||||
QString remoteFolderId = folderId;
|
||||
bool localFolderId = false;
|
||||
folderId.toInt(&localFolderId);
|
||||
if (localFolderId) {
|
||||
const auto folder = FolderDao::findById(folderId.toInt());
|
||||
if (folder) {
|
||||
remoteFolderId = folder->parentFolderId();
|
||||
if (remoteFolderId.isEmpty()) {
|
||||
const QString name = folder->name().toLower();
|
||||
if (name == QStringLiteral("inbox")) remoteFolderId = QStringLiteral("inbox");
|
||||
else if (name == QStringLiteral("sent") || name == QStringLiteral("sent items")) remoteFolderId = QStringLiteral("sentitems");
|
||||
else if (name == QStringLiteral("drafts")) remoteFolderId = QStringLiteral("drafts");
|
||||
else if (name == QStringLiteral("trash") || name == QStringLiteral("deleted items")) remoteFolderId = QStringLiteral("deleteditems");
|
||||
else remoteFolderId = folder->name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QNetworkAccessManager network;
|
||||
auto get = [&](const QUrl &url, QByteArray &data) {
|
||||
QNetworkRequest request = createAuthRequest(url.toString());
|
||||
request.setRawHeader("Accept", "message/rfc822, application/json");
|
||||
QNetworkReply *reply = network.get(request);
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
const bool ok = reply->error() == QNetworkReply::NoError;
|
||||
if (ok) data = reply->readAll();
|
||||
else qWarning() << "Outlook request failed:" << reply->errorString() << url;
|
||||
reply->deleteLater();
|
||||
return ok;
|
||||
};
|
||||
|
||||
QVector<QJsonObject> messageRefs;
|
||||
QUrl nextUrl(buildGraphUrl(QStringLiteral("/me/mailFolders/%1/messages").arg(remoteFolderId)));
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("$top", "50"); // Limitar a 50 mensajes por petición
|
||||
query.addQueryItem("$orderby", "receivedDateTime DESC");
|
||||
url += "?" + query.toString();
|
||||
|
||||
QNetworkRequest request = createAuthRequest(url);
|
||||
QNetworkReply* reply = m_networkManager->get(request);
|
||||
|
||||
// Nota: En una implementación real, esperaríamos la respuesta asíncronamente
|
||||
// pero por simplicidad en este stub, simulamos una respuesta
|
||||
|
||||
// Simular respuesta para desarrollo
|
||||
query.addQueryItem(QStringLiteral("$top"), QStringLiteral("100"));
|
||||
query.addQueryItem(QStringLiteral("$select"), QStringLiteral("id,isRead,importance"));
|
||||
query.addQueryItem(QStringLiteral("$orderby"), QStringLiteral("receivedDateTime DESC"));
|
||||
nextUrl.setQuery(query);
|
||||
while (nextUrl.isValid() && !nextUrl.toString().isEmpty()) {
|
||||
QByteArray response;
|
||||
if (!get(nextUrl, response)) return {};
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !document.isObject()) return {};
|
||||
const QJsonObject object = document.object();
|
||||
for (const QJsonValue &value : object.value(QStringLiteral("value")).toArray())
|
||||
if (value.isObject()) messageRefs.append(value.toObject());
|
||||
const QString continuation = object.value(QStringLiteral("@odata.nextLink")).toString();
|
||||
nextUrl = continuation.isEmpty() ? QUrl() : QUrl(continuation);
|
||||
}
|
||||
|
||||
QVector<MailItem> items;
|
||||
items.append(MailItem(1, folderId.toInt(), "Reunión de proyecto",
|
||||
"juan.perez@empresa.com", m_account.email(),
|
||||
QDateTime::currentDateTime().addSecs(-3600),
|
||||
false, false));
|
||||
items.append(MailItem(2, folderId.toInt(), "Entrega de documentación",
|
||||
"ana.gomez@cliente.com", m_account.email(),
|
||||
QDateTime::currentDateTime().addSecs(-7200),
|
||||
true, false));
|
||||
|
||||
MimeStorageService mimeStorage;
|
||||
for (const QJsonObject &reference : messageRefs) {
|
||||
const QString messageId = reference.value(QStringLiteral("id")).toString();
|
||||
if (messageId.isEmpty()) continue;
|
||||
const QString encodedId = QString::fromUtf8(QUrl::toPercentEncoding(messageId));
|
||||
const QUrl rawUrl(buildGraphUrl(QStringLiteral("/me/messages/%1/$value").arg(encodedId)));
|
||||
QByteArray rawMime;
|
||||
if (!get(rawUrl, rawMime) || rawMime.isEmpty()) continue;
|
||||
ParsedMimeMessage parsed;
|
||||
if (!mimeStorage.parseMessage(rawMime, parsed)) continue;
|
||||
|
||||
MailItem item;
|
||||
item.setFolderId(folderId.toInt());
|
||||
item.setMessageId(messageId);
|
||||
item.setRawMime(rawMime);
|
||||
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
|
||||
item.setSender(parsed.from);
|
||||
item.setRecipient(parsed.to);
|
||||
item.setTo(parsed.to);
|
||||
item.setCc(parsed.cc);
|
||||
item.setBcc(parsed.bcc);
|
||||
item.setDate(parsed.date);
|
||||
item.setBodyHtml(parsed.bodyHtml);
|
||||
item.setSize(rawMime.size());
|
||||
item.setRead(reference.value(QStringLiteral("isRead")).toBool(true));
|
||||
item.setFlagged(reference.value(QStringLiteral("importance")).toString() == QStringLiteral("high"));
|
||||
QVector<QString> attachmentNames;
|
||||
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
|
||||
item.setAttachments(attachmentNames);
|
||||
items.append(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
bool OutlookSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||
{
|
||||
if (!m_account.isTokenValid()) {
|
||||
if (!refreshAccessToken()) {
|
||||
qWarning() << "Failed to refresh token for appending mail item";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Appending mail item to folder:" << folderId;
|
||||
|
||||
// En una implementación real, llamaríamos a Microsoft Graph API
|
||||
// para crear un mensaje en la carpeta especificada
|
||||
|
||||
// Por ahora, simulamos éxito
|
||||
emit mailItemAdded(item);
|
||||
|
||||
// Publish MailItemAddedEvent
|
||||
WinoMail::Events::MailItemAddedEvent mailEvent;
|
||||
mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
||||
mailEvent.timestamp = QDateTime::currentDateTimeUtc();
|
||||
mailEvent.item = item;
|
||||
PUBLISH(mailEvent);
|
||||
|
||||
return true;
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(item);
|
||||
qWarning() << "Outlook appendMailItem is not used for sending; MailService sends via Graph API";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OutlookSynchronizer::updateMailItemFlags(const QString& folderId,
|
||||
const QString& itemUid,
|
||||
bool read, bool flagged)
|
||||
{
|
||||
if (!m_account.isTokenValid()) {
|
||||
if (!refreshAccessToken()) {
|
||||
qWarning() << "Failed to refresh token for updating mail item flags";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Updating mail item flags:" << itemUid
|
||||
<< "read:" << read << "flagged:" << flagged;
|
||||
|
||||
// En una implementación real, llamaríamos a Microsoft Graph API
|
||||
// para actualizar las flags del mensaje
|
||||
|
||||
// Por ahora, simulamos éxito
|
||||
MailItem updatedItem;
|
||||
updatedItem.setId(itemUid.toLongLong());
|
||||
updatedItem.setFolderId(folderId.toInt());
|
||||
updatedItem.setRead(read);
|
||||
updatedItem.setFlagged(flagged);
|
||||
emit mailItemUpdated(updatedItem);
|
||||
|
||||
// Publish MailItemUpdatedEvent
|
||||
WinoMail::Events::MailItemUpdatedEvent updateEvent;
|
||||
updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
||||
updateEvent.timestamp = QDateTime::currentDateTimeUtc();
|
||||
updateEvent.item = updatedItem;
|
||||
updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified
|
||||
PUBLISH(updateEvent);
|
||||
|
||||
return true;
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
Q_UNUSED(read);
|
||||
Q_UNUSED(flagged);
|
||||
qWarning() << "Outlook flag update is not available through this synchronizer";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OutlookSynchronizer::deleteMailItem(const QString& folderId,
|
||||
const QString& itemUid)
|
||||
{
|
||||
if (!m_account.isTokenValid()) {
|
||||
if (!refreshAccessToken()) {
|
||||
qWarning() << "Failed to refresh token for deleting mail item";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Deleting mail item:" << itemUid << "from folder:" << folderId;
|
||||
|
||||
// En una implementación real, llamaríamos a Microsoft Graph API
|
||||
// para eliminar el mensaje
|
||||
|
||||
// Por ahora, simulamos éxito
|
||||
emit mailItemRemoved(itemUid);
|
||||
|
||||
// Publish MailItemRemovedEvent
|
||||
WinoMail::Events::MailItemRemovedEvent removeEvent;
|
||||
removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
||||
removeEvent.timestamp = QDateTime::currentDateTimeUtc();
|
||||
removeEvent.itemUid = itemUid;
|
||||
removeEvent.folderId = folderId.toInt();
|
||||
PUBLISH(removeEvent);
|
||||
|
||||
return true;
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
qWarning() << "Outlook delete is not available through this synchronizer";
|
||||
return false;
|
||||
}
|
||||
|
||||
void OutlookSynchronizer::onGraphReplyFinished(QNetworkReply* reply)
|
||||
@@ -340,9 +336,12 @@ bool OutlookSynchronizer::refreshAccessToken()
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: These should come from secure configuration, not hardcoded
|
||||
const QString clientId = "YOUR_CLIENT_ID_HERE"; // Replace with actual client ID
|
||||
const QString clientSecret = "YOUR_CLIENT_SECRET_HERE"; // Replace with actual client secret
|
||||
const QString clientId = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_ID");
|
||||
const QString clientSecret = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_SECRET");
|
||||
if (clientId.isEmpty() || clientSecret.isEmpty()) {
|
||||
qWarning() << "Outlook token expired and WINO_OUTLOOK_CLIENT_ID/SECRET are not configured";
|
||||
return false;
|
||||
}
|
||||
const QString tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
||||
|
||||
QNetworkRequest networkRequest{QUrl(tokenUrl)};
|
||||
@@ -355,7 +354,8 @@ bool OutlookSynchronizer::refreshAccessToken()
|
||||
postData.addQueryItem("grant_type", "refresh_token");
|
||||
postData.addQueryItem("client_secret", clientSecret);
|
||||
|
||||
QNetworkReply* reply = m_networkManager->post(networkRequest, postData.toString(QUrl::FullyEncoded).toUtf8());
|
||||
QNetworkAccessManager network;
|
||||
QNetworkReply* reply = network.post(networkRequest, postData.toString(QUrl::FullyEncoded).toUtf8());
|
||||
|
||||
// Wait for reply synchronously for simplicity in this context
|
||||
QEventLoop loop;
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
#include "pop3synchronizer.h"
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
#include <QRegularExpression>
|
||||
#include <QSqlQuery>
|
||||
#include <QAbstractSocket>
|
||||
#include <QMap>
|
||||
#include <QCryptographicHash>
|
||||
#include "../../core/mailitem.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../core/models/account.h"
|
||||
#include "../../services/mimestorage.h"
|
||||
|
||||
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
|
||||
: Synchronizer(parent)
|
||||
, m_socket(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::initialize(const Account &account)
|
||||
{
|
||||
m_account = account;
|
||||
const auto &settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
m_useSsl = settings.incomingSsl;
|
||||
m_username = settings.username;
|
||||
m_password = settings.password;
|
||||
|
||||
if (m_socket) {
|
||||
m_socket->deleteLater();
|
||||
m_socket = nullptr;
|
||||
}
|
||||
m_socket = new QSslSocket(this);
|
||||
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::syncFolder(const Folder &folder)
|
||||
{
|
||||
Q_UNUSED(folder);
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<Folder> Pop3Synchronizer::getFolders() const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
Folder inbox;
|
||||
inbox.setId(0);
|
||||
inbox.setAccountId(m_account.id());
|
||||
inbox.setName(QStringLiteral("Inbox"));
|
||||
inbox.setParentFolderId(QString());
|
||||
folders.append(inbox);
|
||||
return folders;
|
||||
}
|
||||
|
||||
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
QSslSocket socket;
|
||||
if (m_useSsl) socket.connectToHostEncrypted(m_host, m_port);
|
||||
else socket.connectToHost(m_host, m_port);
|
||||
if (!socket.waitForConnected(15000)) {
|
||||
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
|
||||
return items;
|
||||
}
|
||||
if (m_useSsl && !socket.waitForEncrypted(15000)) {
|
||||
qWarning() << "TLS handshake failed:" << socket.errorString();
|
||||
return items;
|
||||
}
|
||||
|
||||
auto readLine = [&](QByteArray &line) {
|
||||
if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false;
|
||||
line = socket.readLine();
|
||||
return !line.isEmpty();
|
||||
};
|
||||
auto command = [&](const QByteArray &commandText, QByteArray &response, bool multiline) {
|
||||
socket.write(commandText + QByteArrayLiteral("\r\n"));
|
||||
socket.flush();
|
||||
if (!readLine(response) || !response.trimmed().startsWith('+')) return false;
|
||||
if (multiline) {
|
||||
while (true) {
|
||||
QByteArray line;
|
||||
if (!readLine(line)) return false;
|
||||
if (line == QByteArrayLiteral(".\r\n") || line == QByteArrayLiteral(".\n")) break;
|
||||
if (line.startsWith("..")) line.remove(0, 1);
|
||||
response.append(line);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
QByteArray response;
|
||||
if (!readLine(response) || !response.trimmed().startsWith('+')) return items;
|
||||
if (!command(QByteArrayLiteral("USER ") + m_username.toUtf8(), response, false)
|
||||
|| !command(QByteArrayLiteral("PASS ") + m_password.toUtf8(), response, false)) {
|
||||
qWarning() << "POP3 authentication failed:" << response;
|
||||
return items;
|
||||
}
|
||||
if (!command(QByteArrayLiteral("UIDL"), response, true)) {
|
||||
qWarning() << "UIDL failed:" << response;
|
||||
return items;
|
||||
}
|
||||
|
||||
m_uidToMsgNum.clear();
|
||||
m_maxUid = 0;
|
||||
QList<qint64> uidsToFetch;
|
||||
const QStringList lines = QString::fromUtf8(response).split(QRegularExpression("\\r?\\n"));
|
||||
for (const QString &line : lines) {
|
||||
const QStringList parts = line.trimmed().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
if (parts.size() < 2) continue;
|
||||
bool okMsgId = false, okUid = false;
|
||||
const int msgNum = parts.at(0).toInt(&okMsgId);
|
||||
qint64 uid = parts.at(1).toLongLong(&okUid);
|
||||
if (!okUid) {
|
||||
const QByteArray digest = QCryptographicHash::hash(parts.at(1).toUtf8(), QCryptographicHash::Sha256).toHex().left(15);
|
||||
uid = digest.toLongLong(&okUid, 16);
|
||||
}
|
||||
if (!okMsgId || !okUid || uid <= 0) continue;
|
||||
m_uidToMsgNum[QString::number(uid)] = msgNum;
|
||||
m_maxUid = qMax(m_maxUid, uid);
|
||||
if (uid > sinceUid) uidsToFetch.append(uid);
|
||||
}
|
||||
|
||||
MimeStorageService mimeStorage;
|
||||
int processed = 0;
|
||||
for (const qint64 uid : uidsToFetch) {
|
||||
const int msgNum = m_uidToMsgNum.value(QString::number(uid));
|
||||
QByteArray rawResponse;
|
||||
if (!command(QByteArrayLiteral("RETR ") + QByteArray::number(msgNum), rawResponse, true)) {
|
||||
qWarning() << "RETR failed for message" << msgNum;
|
||||
continue;
|
||||
}
|
||||
const int firstLineEnd = rawResponse.indexOf("\r\n");
|
||||
const QByteArray rawMime = firstLineEnd >= 0 ? rawResponse.mid(firstLineEnd + 2) : QByteArray();
|
||||
ParsedMimeMessage parsed;
|
||||
if (rawMime.isEmpty() || !mimeStorage.parseMessage(rawMime, parsed)) {
|
||||
qWarning() << "Failed to parse POP3 MIME message with UID" << uid;
|
||||
continue;
|
||||
}
|
||||
|
||||
MailItem item;
|
||||
item.setFolderId(folderId.toInt());
|
||||
item.setMessageId(parsed.messageId);
|
||||
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
|
||||
item.setSender(parsed.from);
|
||||
item.setRecipient(parsed.to);
|
||||
item.setTo(parsed.to);
|
||||
item.setCc(parsed.cc);
|
||||
item.setBcc(parsed.bcc);
|
||||
item.setDate(parsed.date);
|
||||
item.setBodyHtml(parsed.bodyHtml);
|
||||
item.setSize(rawMime.size());
|
||||
item.setRawMime(rawMime);
|
||||
item.setUid(uid);
|
||||
QVector<QString> attachmentNames;
|
||||
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
|
||||
item.setAttachments(attachmentNames);
|
||||
items.append(item);
|
||||
++processed;
|
||||
emit progressChanged(uidsToFetch.isEmpty() ? 100 : static_cast<int>((100.0 * processed) / uidsToFetch.size()));
|
||||
emit statusMessage(tr("Fetched %1 of %2 messages").arg(processed).arg(uidsToFetch.size()));
|
||||
}
|
||||
command(QByteArrayLiteral("QUIT"), response, false);
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(item);
|
||||
qWarning() << "POP3 appendMailItem is not a send transport; MailService owns SMTP sending";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
|
||||
{
|
||||
QVector<MailItem> items = MailItemDao::findByFolderId(folderId.toInt());
|
||||
for (MailItem &it : items) {
|
||||
if (it.uid() == itemUid.toLongLong()) {
|
||||
it.setRead(read);
|
||||
it.setFlagged(flagged);
|
||||
if (MailItemDao::update(it))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
if (!m_socket) {
|
||||
qWarning() << "POP3 socket not initialized";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
|
||||
m_socket->disconnectFromHost();
|
||||
if (!m_socket->waitForDisconnected(3000)) {
|
||||
qWarning() << "Failed to disconnect from previous connection";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_useSsl)
|
||||
m_socket->connectToHostEncrypted(m_host, m_port);
|
||||
else
|
||||
m_socket->connectToHost(m_host, m_port);
|
||||
if (!m_socket->waitForConnected(5000)) {
|
||||
qWarning() << "Failed to connect to POP3 server:" << m_socket->errorString();
|
||||
return false;
|
||||
}
|
||||
if (m_useSsl && !m_socket->waitForEncrypted(5000)) {
|
||||
qWarning() << "TLS handshake failed:" << m_socket->errorString();
|
||||
m_socket->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString greeting;
|
||||
if (!m_socket->waitForReadyRead(5000)) {
|
||||
qWarning() << "No greeting from POP3 server";
|
||||
m_socket->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
greeting = QString::fromUtf8(m_socket->readLine()).trimmed();
|
||||
if (!greeting.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "Invalid POP3 greeting:" << greeting;
|
||||
m_socket->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
|
||||
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
|
||||
QString resp;
|
||||
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "USER command failed:" << resp;
|
||||
m_socket->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "PASS command failed:" << resp;
|
||||
m_socket->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get UIDL to map uid to message number
|
||||
QString uidlResp;
|
||||
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "UIDL failed:" << uidlResp;
|
||||
m_socket->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
|
||||
int msgNum = -1;
|
||||
QStringList lines = uidlResp.split(QRegularExpression("\\r?\\n"));
|
||||
for (const QString &line : lines) {
|
||||
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
|
||||
continue;
|
||||
QString l = line;
|
||||
if (l.startsWith(QLatin1Char('+')))
|
||||
l.remove(0, 1);
|
||||
QStringList parts = l.split(QRegularExpression("\\s+"));
|
||||
if (parts.size() < 2)
|
||||
continue;
|
||||
bool okUid;
|
||||
qint64 uid = parts[1].toLongLong(&okUid);
|
||||
if (!okUid)
|
||||
continue;
|
||||
if (uid == itemUid.toLongLong()) {
|
||||
bool okMsgId;
|
||||
msgNum = parts[0].toInt(&okMsgId);
|
||||
if (!okMsgId)
|
||||
msgNum = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msgNum == -1) {
|
||||
qWarning() << "Could not find message number for UID" << itemUid;
|
||||
m_socket->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString deleCmd = QStringLiteral("DELE %1\r\n").arg(msgNum);
|
||||
if (!sendCommand(deleCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "DELE failed for msg" << msgNum << ":" << resp;
|
||||
m_socket->disconnectFromHost();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Deletion takes effect on QUIT
|
||||
sendCommand(QStringLiteral("QUIT\r\n"), resp);
|
||||
m_socket->disconnectFromHost();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString Pop3Synchronizer::readResponse()
|
||||
{
|
||||
if (!m_socket)
|
||||
return QString();
|
||||
if (!m_socket->waitForReadyRead(5000))
|
||||
return QString();
|
||||
return QString::fromUtf8(m_socket->readLine()).trimmed();
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
|
||||
{
|
||||
if (!m_socket)
|
||||
return false;
|
||||
m_socket->write(cmd.toUtf8());
|
||||
if (!m_socket->waitForBytesWritten(5000))
|
||||
return false;
|
||||
response = readResponse();
|
||||
return !response.isEmpty();
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
#include "pop3synchronizer.h"
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
#include <QRegularExpression>
|
||||
#include "../../core/mailitem.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../core/models/account.h"
|
||||
|
||||
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
|
||||
: Synchronizer(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::initialize(const Account &account)
|
||||
{
|
||||
m_account = account;
|
||||
const auto &settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
m_useSsl = settings.incomingSsl;
|
||||
m_username = settings.username;
|
||||
m_password = settings.password;
|
||||
// For simplicity, we do not actually connect here; connection will be made per operation.
|
||||
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::syncFolder(const Folder &folder)
|
||||
{
|
||||
Q_UNUSED(folder);
|
||||
// POP3 does not have folder sync; we just return true.
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<Folder> Pop3Synchronizer::getFolders() const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
Folder inbox;
|
||||
inbox.setId(0); // Assuming folder ID 0 for Inbox (should be looked up)
|
||||
inbox.setAccountId(m_account.id());
|
||||
inbox.setName(QStringLiteral("Inbox"));
|
||||
inbox.setParentFolderId(QString());
|
||||
folders.append(inbox);
|
||||
return folders;
|
||||
}
|
||||
|
||||
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
|
||||
{
|
||||
Q_UNUSED(sinceUid);
|
||||
QVector<MailItem> items;
|
||||
// Connect to POP3 server
|
||||
QSslSocket socket;
|
||||
if (m_useSsl)
|
||||
socket.connectToHostEncrypted(m_host, m_port);
|
||||
else
|
||||
socket.connectToHost(m_host, m_port);
|
||||
if (!socket.waitForConnected(5000)) {
|
||||
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
|
||||
return items;
|
||||
}
|
||||
if (m_useSsl && !socket.waitForEncrypted(5000)) {
|
||||
qWarning() << "TLS handshake failed:" << socket.errorString();
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// Read greeting
|
||||
QString greeting;
|
||||
if (!socket.waitForReadyRead(5000)) {
|
||||
qWarning() << "No greeting from POP3 server";
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
greeting = socket.readLine();
|
||||
if (!greeting.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "Invalid POP3 greeting:" << greeting;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// Login
|
||||
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
|
||||
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
|
||||
QString resp;
|
||||
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "USER command failed:" << resp;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "PASS command failed:" << resp;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// Get list of messages (STAT and LIST)
|
||||
QString statResp;
|
||||
if (!sendCommand(QStringLiteral("STAT\r\n"), statResp) || !statResp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "STAT failed:" << statResp;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// Parse STAT: "+<count> <size>"
|
||||
QStringList statParts = statResp.split(QRegularExpression("\\s+"));
|
||||
int count = 0;
|
||||
if (statParts.size() >= 2)
|
||||
count = statParts[1].toInt();
|
||||
// For simplicity, we fetch all messages (since POP3 doesn't support UID since easily without UIDL mapping)
|
||||
// We'll use UIDL to get unique IDs, then RETR for each.
|
||||
QString uidlResp;
|
||||
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "UIDL failed:" << uidlResp;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// UIDL response lines: each line: "+<msg-id> <uid>"
|
||||
QStringList lines = uidlResp.split(QRegularExpression("\\r?\\n"));
|
||||
for (const QString &line : lines) {
|
||||
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
|
||||
continue;
|
||||
// Remove leading '+'
|
||||
QString l = line;
|
||||
if (l.startsWith(QLatin1Char('+')))
|
||||
l.remove(0,1);
|
||||
QStringList parts = l.split(QRegularExpression("\\s+"));
|
||||
if (parts.size() < 2)
|
||||
continue;
|
||||
QString msgIdStr = parts[0];
|
||||
QString uid = parts[1];
|
||||
bool ok;
|
||||
int msgId = msgIdStr.toInt(&ok);
|
||||
if (!ok)
|
||||
continue;
|
||||
// Retrieve message
|
||||
QString retrCmd = QStringLiteral("RETR %1\r\n").arg(msgIdStr);
|
||||
QString retrResp;
|
||||
if (!sendCommand(retrCmd, retrResp) || !retrResp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "RETR failed for msg" << msgIdStr;
|
||||
continue;
|
||||
}
|
||||
// Read message lines until a line with a single dot.
|
||||
QByteArray msgData;
|
||||
while (true) {
|
||||
if (!socket.waitForReadyRead(5000))
|
||||
break;
|
||||
QByteArray line = socket.readLine();
|
||||
if (line == QByteArray(".\r\n"))
|
||||
break;
|
||||
msgData.append(line);
|
||||
}
|
||||
// Parse rudimentary headers: we can extract Subject, From, Date.
|
||||
// For simplicity, we'll set placeholder values.
|
||||
QString subject = QStringLiteral("(No Subject)");
|
||||
QString from = QStringLiteral("unknown@example.com");
|
||||
QDateTime date = QDateTime::currentDateTimeUtc();
|
||||
bool seen = false; // POP3 doesn't have read flag; we assume unread unless we store locally.
|
||||
bool flagged = false;
|
||||
MailItem item;
|
||||
item.setFolderId(folderId.toInt());
|
||||
item.setSubject(subject);
|
||||
item.setSender(from);
|
||||
item.setDate(date);
|
||||
item.setRead(seen);
|
||||
item.setFlagged(flagged);
|
||||
// We could store the UID in a custom field, but MailItem has uid field (qint64) - we can store the numeric UID if possible.
|
||||
// For now, we leave uid 0.
|
||||
// Insert into DB
|
||||
if (MailItemDao::insert(item)) {
|
||||
QSqlQuery qry;
|
||||
qry.exec(QStringLiteral("SELECT last_insert_rowid()"));
|
||||
qint64 newId = -1;
|
||||
if (qry.next())
|
||||
newId = qry.value(0).toLongLong();
|
||||
item.setId(newId);
|
||||
items.append(item);
|
||||
qDebug() << "Fetched and stored msgId" << msgIdStr << "as mail id" << newId;
|
||||
} else {
|
||||
qWarning() << "Failed to insert mail item for msgId" << msgIdStr;
|
||||
}
|
||||
}
|
||||
// Quit
|
||||
sendCommand(QStringLiteral("QUIT\r\n"), resp);
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(item);
|
||||
// POP3 does not support uploading; we can implement via SMTP elsewhere.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
Q_UNUSED(read);
|
||||
Q_UNUSED(flagged);
|
||||
// POP3 does not support flag updates.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
// POP3 does not support deleting by UID easily without mapping; we could implement DELE by message number.
|
||||
// For simplicity, we return false.
|
||||
return false;
|
||||
}
|
||||
|
||||
QString Pop3Synchronizer::readResponse()
|
||||
{
|
||||
if (!m_socket)
|
||||
return QString();
|
||||
if (!m_socket->waitForReadyRead(5000))
|
||||
return QString();
|
||||
return QString::fromUtf8(m_socket->readLine()).trimmed();
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
|
||||
{
|
||||
if (!m_socket)
|
||||
return false;
|
||||
m_socket->write(cmd.toUtf8());
|
||||
if (!m_socket->waitForBytesWritten(5000))
|
||||
return false;
|
||||
response = readResponse();
|
||||
return !response.isEmpty();
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "pop3synchronizer.h"
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
#include "../../core/mailitem.h"
|
||||
#include "../../db/dao/mailitemdao.h"
|
||||
#include "../../db/dao/folderdao.h"
|
||||
#include "../../core/models/account.h"
|
||||
|
||||
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
|
||||
: Synchronizer(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::initialize(const Account &account)
|
||||
{
|
||||
m_account = account;
|
||||
const auto &settings = account.connectionSettings();
|
||||
m_host = settings.incomingHost;
|
||||
m_port = settings.incomingPort;
|
||||
m_useSsl = settings.incomingSsl;
|
||||
m_username = settings.username;
|
||||
m_password = settings.password;
|
||||
// For simplicity, we do not actually connect here; connection will be made per operation.
|
||||
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::syncFolder(const Folder &folder)
|
||||
{
|
||||
Q_UNUSED(folder);
|
||||
// POP3 does not have folder sync; we just return true.
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<Folder> Pop3Synchronizer::getFolders() const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
Folder inbox;
|
||||
inbox.setId(0); // Assuming folder ID 0 for Inbox (should be looked up)
|
||||
inbox.setAccountId(m_account.id());
|
||||
inbox.setName(QStringLiteral("Inbox"));
|
||||
inbox.setParentId(-1);
|
||||
folders.append(inbox);
|
||||
return folders;
|
||||
}
|
||||
|
||||
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
|
||||
{
|
||||
Q_UNUSED(sinceUid);
|
||||
QVector<MailItem> items;
|
||||
// Connect to POP3 server
|
||||
QSslSocket socket;
|
||||
if (m_useSsl)
|
||||
socket.connectToHostEncrypted(m_host, m_port);
|
||||
else
|
||||
socket.connectToHost(m_host, m_port);
|
||||
if (!socket.waitForConnected(5000)) {
|
||||
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
|
||||
return items;
|
||||
}
|
||||
if (m_useSsl && !socket.waitForEncrypted(5000)) {
|
||||
qWarning() << "TLS handshake failed:" << socket.errorString();
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// Read greeting
|
||||
QString greeting;
|
||||
if (!socket.waitForReadyRead(5000)) {
|
||||
qWarning() << "No greeting from POP3 server";
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
greeting = socket.readLine();
|
||||
if (!greeting.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "Invalid POP3 greeting:" << greeting;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// Login
|
||||
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
|
||||
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
|
||||
QString resp;
|
||||
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "USER command failed:" << resp;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "PASS command failed:" << resp;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// Get list of messages (STAT and LIST)
|
||||
QString statResp;
|
||||
if (!sendCommand(QStringLiteral("STAT\r\n"), statResp) || !statResp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "STAT failed:" << statResp;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// Parse STAT: "+<count> <size>"
|
||||
QStringList statParts = statResp.split(QRegExp("\\s+"));
|
||||
int count = 0;
|
||||
if (statParts.size() >= 2)
|
||||
count = statParts[1].toInt();
|
||||
// For simplicity, we fetch all messages (since POP3 doesn't support UID since easily without UIDL mapping)
|
||||
// We'll use UIDL to get unique IDs, then RETR for each.
|
||||
QString uidlResp;
|
||||
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "UIDL failed:" << uidlResp;
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
// UIDL response lines: each line: "+<msg-id> <uid>"
|
||||
QStringList lines = uidlResp.split(QRegExp("\\r?\\n"));
|
||||
for (const QString &line : lines) {
|
||||
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
|
||||
continue;
|
||||
// Remove leading '+'
|
||||
QString l = line;
|
||||
if (l.startsWith(QLatin1Char('+')))
|
||||
l.remove(0,1);
|
||||
QStringList parts = l.split(QRegExp("\\s+"));
|
||||
if (parts.size() < 2)
|
||||
continue;
|
||||
QString msgIdStr = parts[0];
|
||||
QString uid = parts[1];
|
||||
bool ok;
|
||||
int msgId = msgIdStr.toInt(&ok);
|
||||
if (!ok)
|
||||
continue;
|
||||
// Retrieve message
|
||||
QString retrCmd = QStringLiteral("RETR %1\r\n").arg(msgIdStr);
|
||||
QString retrResp;
|
||||
if (!sendCommand(retrCmd, retrResp) || !retrResp.startsWith(QLatin1Char('+'))) {
|
||||
qWarning() << "RETR failed for msg" << msgIdStr;
|
||||
continue;
|
||||
}
|
||||
// Read message lines until a line with a single dot.
|
||||
QByteArray msgData;
|
||||
while (true) {
|
||||
if (!socket.waitForReadyRead(5000))
|
||||
break;
|
||||
QByteArray line = socket.readLine();
|
||||
if (line == QByteArray(".\r\n"))
|
||||
break;
|
||||
msgData.append(line);
|
||||
}
|
||||
// Parse rudimentary headers: we can extract Subject, From, Date.
|
||||
// For simplicity, we'll set placeholder values.
|
||||
QString subject = QStringLiteral("(No Subject)");
|
||||
QString from = QStringLiteral("unknown@example.com");
|
||||
QDateTime date = QDateTime::currentDateTimeUtc();
|
||||
bool seen = false; // POP3 doesn't have read flag; we assume unread unless we store locally.
|
||||
bool flagged = false;
|
||||
MailItem item;
|
||||
item.setFolderId(folderId.toInt());
|
||||
item.setSubject(subject);
|
||||
item.setFrom(from);
|
||||
item.setDate(date);
|
||||
item.setRead(seen);
|
||||
item.setFlagged(flagged);
|
||||
// We could store the UID in a custom field, but MailItem has uid field (qint64) - we can store the numeric UID if possible.
|
||||
// For now, we leave uid 0.
|
||||
// Insert into DB
|
||||
if (MailItemDao::insert(item)) {
|
||||
QSqlQuery qry;
|
||||
qry.exec(QStringLiteral("SELECT last_insert_rowid()"));
|
||||
qint64 newId = -1;
|
||||
if (qry.next())
|
||||
newId = qry.value(0).toLongLong();
|
||||
item.setId(newId);
|
||||
items.append(item);
|
||||
qDebug() << "Fetched and stored msgId" << msgIdStr << "as mail id" << newId;
|
||||
} else {
|
||||
qWarning() << "Failed to insert mail item for msgId" << msgIdStr;
|
||||
}
|
||||
}
|
||||
// Quit
|
||||
sendCommand(QStringLiteral("QUIT\r\n"), resp);
|
||||
socket.disconnectFromHost();
|
||||
return items;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(item);
|
||||
// POP3 does not support uploading; we can implement via SMTP elsewhere.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
Q_UNUSED(read);
|
||||
Q_UNUSED(flagged);
|
||||
// POP3 does not support flag updates.
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
// POP3 does not support deleting by UID easily without mapping; we could implement DELE by message number.
|
||||
// For simplicity, we return false.
|
||||
return false;
|
||||
}
|
||||
|
||||
QString Pop3Synchronizer::readResponse()
|
||||
{
|
||||
if (!m_socket)
|
||||
return QString();
|
||||
if (!m_socket->waitForReadyRead(5000))
|
||||
return QString();
|
||||
return QString::fromUtf8(m_socket->readLine()).trimmed();
|
||||
}
|
||||
|
||||
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
|
||||
{
|
||||
if (!m_socket)
|
||||
return false;
|
||||
m_socket->write(cmd.toUtf8());
|
||||
if (!m_socket->waitForBytesWritten(5000))
|
||||
return false;
|
||||
response = readResponse();
|
||||
return !response.isEmpty();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "../synchronizer.h"
|
||||
#include <QObject>
|
||||
#include <QSslSocket>
|
||||
#include <QStringList>
|
||||
#include <QMap>
|
||||
|
||||
class Pop3Synchronizer : public Synchronizer
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Pop3Synchronizer(QObject *parent = nullptr);
|
||||
~Pop3Synchronizer() override = default;
|
||||
|
||||
// Synchronizer interface
|
||||
bool initialize(const Account &account) override;
|
||||
bool syncFolder(const Folder &folder) override;
|
||||
QVector<Folder> getFolders() const override;
|
||||
QVector<MailItem> fetchMailItems(const QString &folderId, qint64 sinceUid = 0) override;
|
||||
bool appendMailItem(const QString &folderId, const MailItem &item) override;
|
||||
bool updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) override;
|
||||
bool deleteMailItem(const QString &folderId, const QString &itemUid) override;
|
||||
|
||||
signals:
|
||||
void progressChanged(int percent);
|
||||
void statusMessage(const QString &message);
|
||||
|
||||
private:
|
||||
QString m_host;
|
||||
quint16 m_port{0};
|
||||
bool m_useSsl{false};
|
||||
QString m_username;
|
||||
QString m_password;
|
||||
|
||||
QSslSocket *m_socket{nullptr};
|
||||
// Mapping from UIDL (as string) to message number for DELE
|
||||
QMap<QString, int> m_uidToMsgNum;
|
||||
// Highest UID seen (used for sinceUid)
|
||||
qint64 m_maxUid{0};
|
||||
|
||||
QString readResponse();
|
||||
bool sendCommand(const QString &cmd, QString &response);
|
||||
};
|
||||
Reference in New Issue
Block a user