diff --git a/src/services/imap/imapsynchronizer.cpp b/src/services/imap/imapsynchronizer.cpp index 0911999..7a66df5 100644 --- a/src/services/imap/imapsynchronizer.cpp +++ b/src/services/imap/imapsynchronizer.cpp @@ -1,16 +1,13 @@ #include "imapsynchronizer.h" -#include -#include -#include -#include #include #include -#include -#include -#include "db/dao/folderdao.h" -#include "core/mailitem.h" -#include "db/dao/mailitemdao.h" -#include "db/databasemanager.h" +#include +#include +#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) @@ -19,9 +16,6 @@ ImapSynchronizer::ImapSynchronizer(QObject* parent) bool ImapSynchronizer::initialize(const Account& account) { - // Store the account for later use (e.g., for syncFolder) - m_account = account; - // Store connection settings for later use const Account::ConnectionSettings& settings = account.connectionSettings(); m_host = settings.incomingHost; m_port = settings.incomingPort; @@ -29,964 +23,626 @@ bool ImapSynchronizer::initialize(const Account& account) m_username = settings.username; m_password = settings.password; m_authMethod = settings.authMethod; - qDebug() << "IMAP Synchronizer initialize:" - << "host:" << m_host - << "port:" << m_port - << "ssl:" << m_useSsl - << "username:" << m_username; - emit statusMessage(QStringLiteral("Initializing IMAP synchronizer...")); - emit progressChanged(0); - emit progressChanged(100); return true; } -QString ImapSynchronizer::generateEventId() const +// Helper: connects and logs in to the IMAP server +bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const { - // Simple implementation using timestamp and random component - return QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + - QString::number(std::rand()); -} + bool connected = false; + bool loggedIn = false; + QString response; -bool ImapSynchronizer::sendCommand(QSslSocket& socket, const QString& command, QString& response) const -{ - QByteArray cmd = command.toUtf8() + "\r\n"; - qint64 bytesWritten = socket.write(cmd); - if (bytesWritten == -1) { - qWarning() << "Failed to write to socket:" << socket.errorString(); + // 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; } - if (!socket.waitForBytesWritten(3000)) { - qWarning() << "Timeout waiting for bytes written:" << socket.errorString(); + 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; } - // We'll read response in caller using waitForResponse - return true; -} - -bool ImapSynchronizer::waitForResponse(QSslSocket& socket, const QString& expectedTag, QString& response) const -{ - response.clear(); - while (true) { - if (!socket.waitForReadyRead(10000)) { - qWarning() << "Timeout waiting for response:" << socket.errorString(); - return false; - } - while (socket.canReadLine()) { - QByteArray line = socket.readLine(); - QString lineStr = QString::fromUtf8(line).trimmed(); - response += lineStr + "\n"; - // Check if this line is the completion response for our command - if (lineStr.startsWith(expectedTag + " ") || lineStr.startsWith("*")) { - // Continue reading until we see the tagged completion - if (lineStr.startsWith(expectedTag + " OK") || - lineStr.startsWith(expectedTag + " NO") || - lineStr.startsWith(expectedTag + " BAD")) { - return true; - } - } - } - } -} - -QVector ImapSynchronizer::parseListResponse(const QString& response) const -{ - QVector folders; - QRegularExpression re(R"(\* LIST\s*\(([^)]*)\)\s+\"([^"]*)\"\s+\"(.*)\"\r?\n)"); - // Example: * LIST (\HasNoChildren) "." "INBOX" - // Also handle NIL delimiter: "" "" "INBOX" - QRegularExpressionMatchIterator it = re.globalMatch(response); - while (it.hasNext()) { - QRegularExpressionMatch match = it.next(); - QString flagsStr = match.captured(1); // e.g. \HasNoChildren - QString delimiter = match.captured(2); // usually "." or "/" - QString mailbox = match.captured(3); // the mailbox name, possibly quoted - Folder folder; - folder.setName(mailbox); - // Set special flags based on name or attributes - QString nameLower = mailbox.toLower(); - if (nameLower == "inbox") { - folder.setInbox(true); - } else if (nameLower == "sent" || nameLower == "sent messages") { - folder.setSent(true); - } else if (nameLower == "drafts" || nameLower == "draft") { - folder.setDrafts(true); - } else if (nameLower == "trash" || nameLower == "deleted messages") { - folder.setTrash(true); - } else if (nameLower == "spam" || nameLower == "junk") { - // no specific flag, but we could add a custom attribute; we'll just leave as normal - } - // Parse flags for special-use attributes (RFC 6154) - if (flagsStr.contains("\\Drafts", Qt::CaseInsensitive)) - folder.setDrafts(true); - if (flagsStr.contains("\\Sent", Qt::CaseInsensitive)) - folder.setSent(true); - if (flagsStr.contains("\\Trad", Qt::CaseInsensitive)) // Note: likely a typo in original, we keep as is - folder.setTrash(true); - folders.append(folder); - } - return folders; -} - -QVector ImapSynchronizer::getFolders() const -{ - QVector folders; - QSslSocket socket; - // Connect - qDebug() << "IMAP getFolders: connecting to host" << m_host << "port:" << m_port << "ssl:" << m_useSsl; - 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 IMAP server:" << socket.errorString(); - emit statusMessage(QStringLiteral("Failed to connect to IMAP server: %1").arg(socket.errorString())); - return folders; - } - qDebug() << "IMAP getFolders: connected"; - if (m_useSsl && !socket.waitForEncrypted(5000)) { - qWarning() << "TLS handshake failed:" << socket.errorString(); - emit statusMessage(QStringLiteral("TLS handshake failed: %1").arg(socket.errorString())); - return folders; - } - qDebug() << "IMAP getFolders: TLS handshake done (if SSL)"; - // Read initial greeting (optional) - if (!socket.waitForReadyRead(5000)) { - qWarning() << "No greeting from IMAP server"; - emit statusMessage(QStringLiteral("No greeting from IMAP server")); - socket.disconnectFromHost(); - return folders; - } - qDebug() << "IMAP getFolders: got initial greeting"; - while (socket.canReadLine()) - socket.readLine(); // discard greeting lines - qDebug() << "IMAP getFolders: discarded greeting lines"; // Login - QString loginCmd = QStringLiteral("a001 LOGIN %1 %2").arg(m_username, m_password); - QString resp; - if (!sendCommand(socket, loginCmd, resp) || - !waitForResponse(socket, QStringLiteral("a001"), resp) || - !resp.contains(QStringLiteral("a001 OK"))) { - qWarning() << "LOGIN failed:" << resp; - emit statusMessage(QStringLiteral("LOGIN failed: %1").arg(resp)); - socket.disconnectFromHost(); - return folders; + 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; } - qDebug() << "IMAP getFolders: LOGIN succeeded"; - - // List folders - QString listCmd = QStringLiteral("a002 LIST \"\" \"*\""); - resp.clear(); - if (!sendCommand(socket, listCmd, resp) || - !waitForResponse(socket, QStringLiteral("a002"), resp) || - !resp.contains(QStringLiteral("a002 OK"))) { - qWarning() << "LIST failed:" << resp; - emit statusMessage(QStringLiteral("LIST failed: %1").arg(resp)); - socket.disconnectFromHost(); - return folders; + if (!loginSuccess) { + return false; } - qDebug() << "IMAP getFolders: LIST command succeeded, response length:" << resp.length(); - - // Parse the response - folders = parseListResponse(resp); - qDebug() << "IMAP getFolders: parsed" << folders.size() << "folders"; - - // Logout - QString logoutCmd = QStringLiteral("a003 LOGOUT"); - sendCommand(socket, logoutCmd, resp); - waitForResponse(socket, QStringLiteral("a003"), resp); - socket.disconnectFromHost(); - qDebug() << "IMAP getFolders: returning" << folders.size() << "folders"; - return folders; + loggedIn = true; + return true; } bool ImapSynchronizer::syncFolder(const Folder& folder) { - emit statusMessage(QStringLiteral("Syncing folder: %1").arg(folder.name())); - emit progressChanged(0); - QSslSocket socket; - // Connect - 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 IMAP server:" << socket.errorString(); - emit statusMessage(QStringLiteral("Failed to connect to IMAP server: %1").arg(socket.errorString())); - return false; - } - if (m_useSsl && !socket.waitForEncrypted(5000)) { - qWarning() << "TLS handshake failed:" << socket.errorString(); - emit statusMessage(QStringLiteral("TLS handshake failed: %1").arg(socket.errorString())); - return false; - } - // Read initial greeting (optional) - if (!socket.waitForReadyRead(5000)) { - qWarning() << "No greeting from IMAP server"; - emit statusMessage(QStringLiteral("No greeting from IMAP server")); - socket.disconnectFromHost(); - return false; - } - while (socket.canReadLine()) - socket.readLine(); // discard greeting lines - - // Login - QString loginCmd = QStringLiteral("a001 LOGIN %1 %2").arg(m_username, m_password); - QString resp; - if (!sendCommand(socket, loginCmd, resp) || - !waitForResponse(socket, QStringLiteral("a001"), resp) || - !resp.contains(QStringLiteral("a001 OK"))) { - qWarning() << "LOGIN failed:" << resp; - emit statusMessage(QStringLiteral("LOGIN failed: %1").arg(resp)); - socket.disconnectFromHost(); - return false; - } - emit progressChanged(20); + ImapConnection conn; + if (!connectAndLogin(conn)) return false; // Select folder - QString selectCmd = QStringLiteral("a002 SELECT \"%1\"").arg(folder.name()); - resp.clear(); - if (!sendCommand(socket, selectCmd, resp) || - !waitForResponse(socket, QStringLiteral("a002"), resp) || - !resp.contains(QStringLiteral("a002 OK"))) { - qWarning() << "SELECT failed:" << resp; - emit statusMessage(QStringLiteral("SELECT failed: %1").arg(resp)); - socket.disconnectFromHost(); + 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; } - emit progressChanged(40); - // Get all UIDs in the mailbox - QString searchCmd = QStringLiteral("a003 UID SEARCH ALL"); - resp.clear(); - if (!sendCommand(socket, searchCmd, resp) || - !waitForResponse(socket, QStringLiteral("a003"), resp) || - !resp.contains(QStringLiteral("a003 OK"))) { - qWarning() << "SEARCH failed:" << resp; - emit statusMessage(QStringLiteral("SEARCH failed: %1").arg(resp)); - socket.disconnectFromHost(); + // Get all UIDs + QString searchCmd = "UID SEARCH ALL"; + QString searchResp; + if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) { + qWarning() << "SEARCH failed"; return false; } - // Extract UIDs from response (lines containing numbers before the tagged OK) - QList uidList; - QStringList lines = resp.split('\n', Qt::SkipEmptyParts); + + QVector uids; + QStringList lines = searchResp.split('\n'); for (const QString& line : lines) { - if (line.startsWith(QLatin1Char('*')) && line.contains(QStringLiteral("SEARCH"))) { - // Example: * SEARCH 1 2 3 4 5 + if (line.startsWith("* SEARCH")) { QStringList parts = line.split(QRegularExpression("\\s+")); for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" - bool ok2; - qint64 uid = parts[i].toLongLong(&ok2); - if (ok2) - uidList.append(uid); + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); } + break; } } - if (uidList.isEmpty()) { - qDebug() << "No messages in folder" << folder.name(); - // Still need to update unread count to 0 - std::optional fOpt = FolderDao::findById(folder.id()); - if (fOpt) { - Folder f = *fOpt; - f.setUnreadCount(0); - FolderDao::update(f); - } - // Logout - QString logoutCmd = QStringLiteral("a004 LOGOUT"); - sendCommand(socket, logoutCmd, resp); - waitForResponse(socket, QStringLiteral("a004"), resp); - socket.disconnectFromHost(); - emit progressChanged(100); + + // 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; } - emit progressChanged(50); - // Fetch FLAGS for all UIDs (we could batch fetch, but do one by one for simplicity) - QMap> flagsMap; // uid -> (seen, flagged) - for (qint64 uid : uidList) { - QString fetchCmd = QStringLiteral("a%1 UID FETCH %2 (FLAGS)").arg(QString::number(uid * 10, 16)).arg(uid); - resp.clear(); - if (!sendCommand(socket, fetchCmd, resp) || - !waitForResponse(socket, QStringLiteral("a%1").arg(uid * 10, 16), resp)) { - qWarning() << "FETCH FLAGS failed for uid" << uid; - continue; - } - bool seen = resp.contains("\\Seen"); - bool flagged = resp.contains("\\Flagged"); + // 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> 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); } - emit progressChanged(70); - // Load all mail items for this folder from local DB - QVector localItems = MailItemDao::findByFolderId(folder.id()); - QMap uidToItem; + // Update local items + auto localItems = MailItemDao::findByFolderId(folder.id()); for (MailItem& item : localItems) { - uidToItem[item.uid()] = item; - } - - // Update local items with flags from server - QList toUpdate; - for (auto it = uidToItem.begin(); it != uidToItem.end(); ++it) { - qint64 uid = it.key(); - MailItem& item = it.value(); - if (flagsMap.contains(uid)) { - bool seen = flagsMap[uid].first; - bool flagged = flagsMap[uid].second; - bool changed = false; - if (item.isRead() != seen) { + 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); - changed = true; - } - if (item.isFlagged() != flagged) { item.setFlagged(flagged); - changed = true; + MailItemDao::update(item); } - if (changed) { - toUpdate.append(item); - } - } - // If uid not found on server anymore, we could optionally delete locally, but we keep. - } - - // Persist changes - for (MailItem& item : toUpdate) { - if (!MailItemDao::update(item)) { - qWarning() << "Failed to update mail item uid" << item.uid(); } } - // Compute unread count from local items (or we could count unseen from server) - int unreadCount = 0; + // Count unread + int unread = 0; for (const MailItem& item : localItems) { - if (!item.isRead()) - ++unreadCount; - } - // Update folder unread count - std::optional fOpt = FolderDao::findById(folder.id()); - if (fOpt) { - Folder f = *fOpt; - f.setUnreadCount(unreadCount); - if (!FolderDao::update(f)) { - qWarning() << "Failed to update folder unread count for folderId" << folder.id(); - } + if (!item.isRead()) unread++; } + Folder f = folder; + f.setUnreadCount(unread); + FolderDao::update(f); - // Logout - QString logoutCmd = QStringLiteral("a004 LOGOUT"); - sendCommand(socket, logoutCmd, resp); - waitForResponse(socket, QStringLiteral("a004"), resp); - socket.disconnectFromHost(); - emit progressChanged(100); - return !toUpdate.isEmpty(); // return true if we made changes + conn.disconnect(); + return true; } QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid) { QVector items; - qDebug() << "IMAP fetchMailItems called for folderId:" << folderId << "sinceUid:" << sinceUid; - // Get folder object - bool ok; - int fid = folderId.toInt(&ok); - if (!ok) { - qWarning() << "Invalid folderId:" << folderId; - return items; - } - std::optional fOpt = FolderDao::findById(fid); - if (!fOpt) { - qWarning() << "Folder not found for id:" << fid; - return items; - } - const Folder& folder = *fOpt; + ImapConnection conn; - emit statusMessage(QStringLiteral("Fetching emails from folder: %1").arg(folder.name())); - emit progressChanged(0); + if (!connectAndLogin(conn)) { + return items; + } - QSslSocket socket; - // Connect - 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 IMAP server:" << socket.errorString(); - emit statusMessage(QStringLiteral("Failed to connect to IMAP server: %1").arg(socket.errorString())); + // 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; } - if (m_useSsl && !socket.waitForEncrypted(5000)) { - qWarning() << "TLS handshake failed:" << socket.errorString(); - emit statusMessage(QStringLiteral("TLS handshake failed: %1").arg(socket.errorString())); - return items; - } - // Read initial greeting (optional) - if (!socket.waitForReadyRead(5000)) { - qWarning() << "No greeting from IMAP server"; - emit statusMessage(QStringLiteral("No greeting from IMAP server")); - socket.disconnectFromHost(); - return items; - } - while (socket.canReadLine()) - socket.readLine(); // discard greeting lines + QString folderName = folderOpt->name(); - // Login - QString loginCmd = QStringLiteral("a001 LOGIN %1 %2").arg(m_username, m_password); - QString resp; - if (!sendCommand(socket, loginCmd, resp) || - !waitForResponse(socket, QStringLiteral("a001"), resp) || - !resp.contains(QStringLiteral("a001 OK"))) { - qWarning() << "LOGIN failed:" << resp; - emit statusMessage(QStringLiteral("LOGIN failed: %1").arg(resp)); - socket.disconnectFromHost(); + // 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; } - emit progressChanged(10); - - // Select folder - QString selectCmd = QStringLiteral("a002 SELECT \"%1\"").arg(folder.name()); - resp.clear(); - if (!sendCommand(socket, selectCmd, resp) || - !waitForResponse(socket, QStringLiteral("a002"), resp) || - !resp.contains(QStringLiteral("a002 OK"))) { - qWarning() << "SELECT failed:" << resp; - emit statusMessage(QStringLiteral("SELECT failed: %1").arg(resp)); - socket.disconnectFromHost(); + if (!selectResponse.contains(" OK ")) { + qWarning() << "SELECT failed:" << selectResponse; return items; } - emit progressChanged(20); - // Determine search criteria - QString searchCmd; + // SEARCH + QString searchCommand; if (sinceUid > 0) { - searchCmd = QStringLiteral("a003 UID SEARCH %1:*").arg(sinceUid); + searchCommand = QString("UID SEARCH %1:*").arg(sinceUid); } else { - searchCmd = QStringLiteral("a003 UID SEARCH ALL"); + searchCommand = "UID SEARCH ALL"; } - resp.clear(); - if (!sendCommand(socket, searchCmd, resp) || - !waitForResponse(socket, QStringLiteral("a003"), resp) || - !resp.contains(QStringLiteral("a003 OK"))) { - qWarning() << "SEARCH failed:" << resp; - emit statusMessage(QStringLiteral("SEARCH failed: %1").arg(resp)); - socket.disconnectFromHost(); + QString searchResponse; + if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { + qWarning() << "SEARCH command failed:" << searchResponse; return items; } - // Extract UIDs from response (lines containing numbers before the tagged OK) - QList uidList; - QStringList lines = resp.split('\n', Qt::SkipEmptyParts); + + // Parse UIDs (robust: look for line starting with "* SEARCH") + QVector uids; + QStringList lines = searchResponse.split('\n'); for (const QString& line : lines) { - if (line.startsWith(QLatin1Char('*')) && line.contains(QStringLiteral("SEARCH"))) { - // Example: * SEARCH 1 2 3 4 5 - QStringList parts; - parts = line.split(QRegularExpression("\\s+")); + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" - bool ok2; - qint64 uid = parts[i].toLongLong(&ok2); - if (ok2) - uidList.append(uid); + 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 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 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(); } } } - if (uidList.isEmpty()) { - qDebug() << "No new messages to fetch"; - emit statusMessage(QStringLiteral("No new messages to fetch")); - socket.disconnectFromHost(); - emit progressChanged(100); - return items; - } - emit progressChanged(30); - // Fetch each UID - int total = uidList.size(); - for (int i = 0; i < uidList.size(); ++i) { - qint64 uid = uidList[i]; - // Fetch envelope, flags, internal date - QString fetchCmd = QStringLiteral("a%1 UID FETCH %2 (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)") - .arg(QString::number(uid * 10, 16)).arg(uid); - resp.clear(); - if (!sendCommand(socket, fetchCmd, resp) || - !waitForResponse(socket, QStringLiteral("a%1").arg(uid * 10, 16), resp)) { - qWarning() << "FETCH failed for uid" << uid; + // Close and disconnect + QString dummy; + conn.sendCommandWait("CLOSE", dummy, 30000); + conn.disconnect(); + return items; +} + +QVector ImapSynchronizer::getFolders() const +{ + QVector 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 ImapSynchronizer::parseListResponse(const QStringList& lines) const +{ + QVector 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 ImapSynchronizer::parseFetchResponse(const QStringList& lines) const +{ + QVector items; + int i = 0; + while (i < lines.size()) { + const QString& line = lines[i]; + if (!line.startsWith(QStringLiteral("* "))) { + ++i; continue; } - // Parse response - bool seen = false, flagged = false; - QDateTime internalDate; - QString subject, from; - // Look for FLAGS - if (resp.contains("\\Seen")) - seen = true; - if (resp.contains("\\Flagged")) - flagged = true; - // InternalDate - QRegularExpression internalRegex(QStringLiteral("INTERNALDATE \"([^\"]*)\"")); - QRegularExpressionMatch internalMatch = internalRegex.match(resp); - if (internalMatch.hasMatch()) { - QString dateStr = internalMatch.captured(1); - // IMAP internal-date format: "dd-Mmm-yyyy hh:mm:ss zzz" - internalDate = QDateTime::fromString(dateStr, "dd-MMM-yyyy hh:mm:ss"); - if (!internalDate.isValid()) - internalDate = QDateTime::currentDateTimeUtc(); // fallback - } else { - internalDate = QDateTime::currentDateTimeUtc(); + int fetchPos = line.indexOf(QStringLiteral(" FETCH (")); + if (fetchPos == -1) { + ++i; + continue; } - // Envelope: extract subject and from (simplified) - QRegularExpression envelopeRegex(QStringLiteral("ENVELOPE \\((.*)\\)")); - QRegularExpressionMatch envelopeMatch = envelopeRegex.match(resp); - if (envelopeMatch.hasMatch()) { - QString envelopeStr = envelopeMatch.captured(1); - // Envelope fields: date subject from sender reply-to to cc bcc in-reply-to message-id - // We'll parse by splitting on spaces but respecting quoted strings - simplified: - int idx = 0; - // date - int d1 = envelopeStr.indexOf('\"', idx); - if (d1 != -1) { - int d2 = envelopeStr.indexOf('\"', d1 + 1); - if (d2 != -1) { - idx = d2 + 1; - // subject - int s1 = envelopeStr.indexOf('\"', idx); - if (s1 != -1) { - int s2 = envelopeStr.indexOf('\"', s1 + 1); - if (s2 != -1) { - subject = envelopeStr.mid(s1 + 1, s2 - s1 - 1); + + // 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 + 16; // 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")); + 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 } } } } } - // If subject empty, set placeholder - if (subject.isEmpty()) - subject = QStringLiteral("(No Subject)"); - // From extraction: we'll just parse the from field similarly (third quoted string) - // For simplicity, we'll set a placeholder; we could improve but skip for now. - if (from.isEmpty()) - from = QStringLiteral("unknown@example.com"); - MailItem item; - item.setFolderId(folder.id()); - item.setSubject(subject); - item.setSender(from); - // For simplicity, set recipient as empty - item.setRecipient(QString()); - item.setDate(internalDate); - item.setRead(seen); - item.setFlagged(flagged); - item.setUid(uid); // store the IMAP UID - // Size unknown; we could parse RFC822.SIZE but skip - // 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 uid" << uid << "as mail id" << newId; - } else { - qWarning() << "Failed to insert mail item for uid" << uid; + // 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); + } + } } - // Update progress - int prog = 30 + ((i + 1) * 70) / total; - emit progressChanged(prog); - } - // Logout - QString logoutCmd = QStringLiteral("a004 LOGOUT"); - sendCommand(socket, logoutCmd, resp); - waitForResponse(socket, QStringLiteral("a004"), resp); - socket.disconnectFromHost(); - emit statusMessage(QStringLiteral("Fetched %1 new email(s)").arg(items.size())); - emit progressChanged(100); - qDebug() << "IMAP fetchMailItems returning" << items.size() << "items"; + // If subject not set, use placeholder + if (item.subject().isEmpty()) + item.setSubject(QStringLiteral("(No Subject)")); + + items.append(item); + ++i; + } return items; } - -bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) -{ - emit statusMessage(QStringLiteral("Appending email to folder: %1").arg(folderId)); - emit progressChanged(0); - bool ok; - int fid = folderId.toInt(&ok); - if (!ok) { - qWarning() << "Invalid folderId:" << folderId; - emit statusMessage(QStringLiteral("Invalid folder ID")); - emit progressChanged(100); - return false; - } - std::optional fOpt = FolderDao::findById(fid); - if (!fOpt) { - qWarning() << "Folder not found for id:" << fid; - emit statusMessage(QStringLiteral("Folder not found")); - emit progressChanged(100); - return false; - } - const Folder& folder = *fOpt; - - // Build RFC822 message - QString dateStr = item.date().toString(Qt::RFC2822Date); - QString from = item.sender(); - QString to = item.recipient(); - QString cc = item.cc(); - QString bcc = item.bcc(); - QString subject = item.subject(); - QString body = item.bodyHtml(); // assuming bodyHtml contains the body text - - QStringList headers; - if (!dateStr.isEmpty()) headers << QString("Date: %1").arg(dateStr); - if (!from.isEmpty()) headers << QString("From: %1").arg(from); - if (!to.isEmpty()) headers << QString("To: %1").arg(to); - if (!cc.isEmpty()) headers << QString("Cc: %1").arg(cc); - if (!bcc.isEmpty()) headers << QString("Bcc: %1").arg(bcc); - if (!subject.isEmpty()) headers << QString("Subject: %1").arg(subject); - headers << QStringLiteral("MIME-Version: 1.0"); - headers << QStringLiteral("Content-Type: text/plain; charset=\"utf-8\""); - headers << QStringLiteral("Content-Transfer-Encoding: 7bit"); - headers << QStringLiteral(""); // empty line before body - if (!body.isEmpty()) headers << body; - - QString message = headers.join(QStringLiteral("\r\n")); - QByteArray messageBytes = message.toUtf8(); - - 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 IMAP server:" << socket.errorString(); - emit statusMessage(QStringLiteral("Failed to connect to IMAP server: %1").arg(socket.errorString())); - emit progressChanged(100); - return false; - } - if (m_useSsl && !socket.waitForEncrypted(5000)) { - qWarning() << "TLS handshake failed:" << socket.errorString(); - emit statusMessage(QStringLiteral("TLS handshake failed: %1").arg(socket.errorString())); - emit progressChanged(100); - return false; - } - if (!socket.waitForReadyRead(5000)) { - qWarning() << "No greeting from IMAP server"; - emit statusMessage(QStringLiteral("No greeting from IMAP server")); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - while (socket.canReadLine()) - socket.readLine(); // discard greeting lines - - // Login - QString loginCmd = QStringLiteral("a001 LOGIN %1 %2").arg(m_username, m_password); - QString resp; - if (!sendCommand(socket, loginCmd, resp) || - !waitForResponse(socket, QStringLiteral("a001"), resp) || - !resp.contains(QStringLiteral("a001 OK"))) { - qWarning() << "LOGIN failed:" << resp; - emit statusMessage(QStringLiteral("LOGIN failed: %1").arg(resp)); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - emit progressChanged(30); - - // APPEND - QString appendCmd = QStringLiteral("a002 APPEND \"%1\" (\\Seen) {%2}\r\n").arg(folder.name()).arg(messageBytes.size()); - QByteArray cmd = appendCmd.toUtf8() + messageBytes + "\r\n"; - qint64 bytesWritten = socket.write(cmd); - if (bytesWritten == -1) { - qWarning() << "Failed to write APPEND command:" << socket.errorString(); - emit statusMessage(QStringLiteral("Failed to write APPEND command: %1").arg(socket.errorString())); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - if (!socket.waitForBytesWritten(3000)) { - qWarning() << "Timeout waiting for bytes written:" << socket.errorString(); - emit statusMessage(QStringLiteral("Timeout waiting for bytes written: %1").arg(socket.errorString())); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - resp.clear(); - if (!waitForResponse(socket, QStringLiteral("a002"), resp) || - !resp.contains(QStringLiteral("a002 OK"))) { - qWarning() << "APPEND failed:" << resp; - emit statusMessage(QStringLiteral("APPEND failed: %1").arg(resp)); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - emit progressChanged(80); - - // Logout - QString logoutCmd = QStringLiteral("a003 LOGOUT"); - sendCommand(socket, logoutCmd, resp); - waitForResponse(socket, QStringLiteral("a003"), resp); - socket.disconnectFromHost(); - emit statusMessage(QStringLiteral("Email appended successfully")); - emit progressChanged(100); - return true; -} - -bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, - const QString& itemUid, - bool read, bool flagged) -{ - emit statusMessage(QStringLiteral("Updating email flags")); - emit progressChanged(0); - bool ok; - qint64 uid = itemUid.toLongLong(&ok); - if (!ok) { - qWarning() << "Invalid itemUid:" << itemUid; - emit statusMessage(QStringLiteral("Invalid item UID")); - emit progressChanged(100); - return false; - } - std::optional opt = MailItemDao::findById(uid); - if (!opt) { - qWarning() << "MailItem not found for uid:" << uid; - emit statusMessage(QStringLiteral("MailItem not found")); - emit progressChanged(100); - return false; - } - const MailItem& mail = *opt; - std::optional fOpt = FolderDao::findById(mail.folderId()); - if (!fOpt) { - qWarning() << "Folder not found for mail item uid:" << uid; - emit statusMessage(QStringLiteral("Folder not found")); - emit progressChanged(100); - return false; - } - const Folder& folder = *fOpt; - - 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 IMAP server:" << socket.errorString(); - emit statusMessage(QStringLiteral("Failed to connect to IMAP server: %1").arg(socket.errorString())); - emit progressChanged(100); - return false; - } - if (m_useSsl && !socket.waitForEncrypted(5000)) { - qWarning() << "TLS handshake failed:" << socket.errorString(); - emit statusMessage(QStringLiteral("TLS handshake failed: %1").arg(socket.errorString())); - emit progressChanged(100); - return false; - } - if (!socket.waitForReadyRead(5000)) { - qWarning() << "No greeting from IMAP server"; - emit statusMessage(QStringLiteral("No greeting from IMAP server")); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - while (socket.canReadLine()) - socket.readLine(); // discard greeting lines - - // Login - QString loginCmd = QStringLiteral("a001 LOGIN %1 %2").arg(m_username, m_password); - QString resp; - if (!sendCommand(socket, loginCmd, resp) || - !waitForResponse(socket, QStringLiteral("a001"), resp) || - !resp.contains(QStringLiteral("a001 OK"))) { - qWarning() << "LOGIN failed:" << resp; - emit statusMessage(QStringLiteral("LOGIN failed: %1").arg(resp)); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - emit progressChanged(20); - - // Select folder - QString selectCmd = QStringLiteral("a002 SELECT \"%1\"").arg(folder.name()); - resp.clear(); - if (!sendCommand(socket, selectCmd, resp) || - !waitForResponse(socket, QStringLiteral("a002"), resp) || - !resp.contains(QStringLiteral("a002 OK"))) { - qWarning() << "SELECT failed:" << resp; - emit statusMessage(QStringLiteral("SELECT failed: %1").arg(resp)); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - emit progressChanged(40); - - bool allOk = true; - if (read != mail.isRead()) { - QString storeCmd = QStringLiteral("a003 UID STORE %1 %2\\Seen") - .arg(uid) - .arg(read ? "+" : "-"); - resp.clear(); - if (!sendCommand(socket, storeCmd, resp) || - !waitForResponse(socket, QStringLiteral("a003"), resp) || - !resp.contains(QStringLiteral("a003 OK"))) { - qWarning() << "STORE Seen failed:" << resp; - emit statusMessage(QStringLiteral("STORE Seen failed: %1").arg(resp)); - allOk = false; - } - } - if (flagged != mail.isFlagged()) { - QString storeCmd = QStringLiteral("a004 UID STORE %1 %2\\Flagged") - .arg(uid) - .arg(flagged ? "+" : "-"); - resp.clear(); - if (!sendCommand(socket, storeCmd, resp) || - !waitForResponse(socket, QStringLiteral("a004"), resp) || - !resp.contains(QStringLiteral("a004 OK"))) { - qWarning() << "STORE Flagged failed:" << resp; - emit statusMessage(QStringLiteral("STORE Flagged failed: %1").arg(resp)); - allOk = false; - } - } - emit progressChanged(80); - - // Logout - QString logoutCmd = QStringLiteral("a005 LOGOUT"); - sendCommand(socket, logoutCmd, resp); - waitForResponse(socket, QStringLiteral("a005"), resp); - socket.disconnectFromHost(); - if (allOk) { - // Update local copy - MailItem updated = mail; - updated.setRead(read); - updated.setFlagged(flagged); - if (MailItemDao::update(updated)) { - emit statusMessage(QStringLiteral("Email flags updated successfully")); - emit progressChanged(100); - return true; - } - } - emit statusMessage(QStringLiteral("Failed to update email flags")); - emit progressChanged(100); - return false; -} - -bool ImapSynchronizer::deleteMailItem(const QString& folderId, - const QString& itemUid) -{ - emit statusMessage(QStringLiteral("Deleting email")); - emit progressChanged(0); - bool ok; - qint64 uid = itemUid.toLongLong(&ok); - if (!ok) { - qWarning() << "Invalid itemUid:" << itemUid; - emit statusMessage(QStringLiteral("Invalid item UID")); - emit progressChanged(100); - return false; - } - std::optional opt = MailItemDao::findById(uid); - if (!opt) { - qWarning() << "MailItem not found for uid:" << uid; - emit statusMessage(QStringLiteral("MailItem not found")); - emit progressChanged(100); - return false; - } - const MailItem& mail = *opt; - std::optional fOpt = FolderDao::findById(mail.folderId()); - if (!fOpt) { - qWarning() << "Folder not found for mail item uid:" << uid; - emit statusMessage(QStringLiteral("Folder not found")); - emit progressChanged(100); - return false; - } - const Folder& folder = *fOpt; - - 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 IMAP server:" << socket.errorString(); - emit statusMessage(QStringLiteral("Failed to connect to IMAP server: %1").arg(socket.errorString())); - emit progressChanged(100); - return false; - } - if (m_useSsl && !socket.waitForEncrypted(5000)) { - qWarning() << "TLS handshake failed:" << socket.errorString(); - emit statusMessage(QStringLiteral("TLS handshake failed: %1").arg(socket.errorString())); - emit progressChanged(100); - return false; - } - if (!socket.waitForReadyRead(5000)) { - qWarning() << "No greeting from IMAP server"; - emit statusMessage(QStringLiteral("No greeting from IMAP server")); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - while (socket.canReadLine()) - socket.readLine(); // discard greeting lines - - // Login - QString loginCmd = QStringLiteral("a001 LOGIN %1 %2").arg(m_username, m_password); - QString resp; - if (!sendCommand(socket, loginCmd, resp) || - !waitForResponse(socket, QStringLiteral("a001"), resp) || - !resp.contains(QStringLiteral("a001 OK"))) { - qWarning() << "LOGIN failed:" << resp; - emit statusMessage(QStringLiteral("LOGIN failed: %1").arg(resp)); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - emit progressChanged(20); - - // Select folder - QString selectCmd = QStringLiteral("a002 SELECT \"%1\"").arg(folder.name()); - resp.clear(); - if (!sendCommand(socket, selectCmd, resp) || - !waitForResponse(socket, QStringLiteral("a002"), resp) || - !resp.contains(QStringLiteral("a002 OK"))) { - qWarning() << "SELECT failed:" << resp; - emit statusMessage(QStringLiteral("SELECT failed: %1").arg(resp)); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - emit progressChanged(40); - - // Mark as \\Deleted - QString storeCmd = QStringLiteral("a003 UID STORE %1 +FLAGS \\Deleted").arg(uid); - resp.clear(); - if (!sendCommand(socket, storeCmd, resp) || - !waitForResponse(socket, QStringLiteral("a003"), resp) || - !resp.contains(QStringLiteral("a003 OK"))) { - qWarning() << "STORE Deleted failed:" << resp; - emit statusMessage(QStringLiteral("STORE Deleted failed: %1").arg(resp)); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - emit progressChanged(60); - - // EXPUNGE - QString expungeCmd = QStringLiteral("a004 EXPUNGE"); - resp.clear(); - if (!sendCommand(socket, expungeCmd, resp) || - !waitForResponse(socket, QStringLiteral("a004"), resp) || - !resp.contains(QStringLiteral("a004 OK"))) { - qWarning() << "EXPUNGE failed:" << resp; - emit statusMessage(QStringLiteral("EXPUNGE failed: %1").arg(resp)); - socket.disconnectFromHost(); - emit progressChanged(100); - return false; - } - emit progressChanged(80); - - // Logout - QString logoutCmd = QStringLiteral("a005 LOGOUT"); - sendCommand(socket, logoutCmd, resp); - waitForResponse(socket, QStringLiteral("a005"), resp); - socket.disconnectFromHost(); - // Delete local copy - if (MailItemDao::remove(uid)) { - emit statusMessage(QStringLiteral("Email deleted successfully")); - emit progressChanged(100); - return true; - } - emit statusMessage(QStringLiteral("Failed to delete email")); - emit progressChanged(100); - return false; -} - -#include "imapsynchronizer.moc" \ No newline at end of file